717 lines
32 KiB
C++
717 lines
32 KiB
C++
|
|
/*
|
||
|
|
* Copyright 2022 Google LLC
|
||
|
|
*
|
||
|
|
* Use of this source code is governed by a BSD-style license that can be
|
||
|
|
* found in the LICENSE file.
|
||
|
|
*/
|
||
|
|
|
||
|
|
#include "include/core/SkCanvas.h"
|
||
|
|
#include "include/core/SkM44.h"
|
||
|
|
#include "include/core/SkPaint.h"
|
||
|
|
#include "include/core/SkRRect.h"
|
||
|
|
#include "include/core/SkVertices.h"
|
||
|
|
#include "include/private/base/SkTPin.h"
|
||
|
|
#include "tools/viewer/ClickHandlerSlide.h"
|
||
|
|
|
||
|
|
#include <unordered_set>
|
||
|
|
|
||
|
|
static SkPaint paint(SkColor color,
|
||
|
|
float strokeWidth = -1.f,
|
||
|
|
SkPaint::Join join = SkPaint::kMiter_Join) {
|
||
|
|
SkPaint paint;
|
||
|
|
paint.setColor(color);
|
||
|
|
paint.setAntiAlias(true);
|
||
|
|
if (strokeWidth >= 0.f) {
|
||
|
|
paint.setStyle(SkPaint::kStroke_Style);
|
||
|
|
paint.setStrokeWidth(strokeWidth);
|
||
|
|
paint.setStrokeJoin(join);
|
||
|
|
}
|
||
|
|
return paint;
|
||
|
|
}
|
||
|
|
|
||
|
|
// Singular values for [a b][c d] 2x2 matrix, unordered.
|
||
|
|
static std::pair<float, float> singular_values(float a, float b, float c, float d) {
|
||
|
|
float s1 = a*a + b*b + c*c + d*d;
|
||
|
|
|
||
|
|
float e = a*a + b*b - c*c - d*d;
|
||
|
|
float f = a*c + b*d;
|
||
|
|
float s2 = SkScalarSqrt(e*e + 4*f*f);
|
||
|
|
|
||
|
|
float singular1 = SkScalarSqrt(0.5f * (s1 + s2));
|
||
|
|
float singular2 = SkScalarSqrt(0.5f * (s1 - s2));
|
||
|
|
|
||
|
|
return {singular1, singular2};
|
||
|
|
}
|
||
|
|
|
||
|
|
static constexpr float kAARadius = 10.f;
|
||
|
|
|
||
|
|
// [m00 m01 * m03] [f(u,v)]
|
||
|
|
// Assuming M = [m10 m11 * m13], define the projected p'(u,v) = [g(u,v)] where
|
||
|
|
// [ * * * * ]
|
||
|
|
// [m30 m31 * m33]
|
||
|
|
// [x] [u]
|
||
|
|
// f(u,v) = x(u,v) / w(u,v), g(u,v) = y(u,v) / w(u,v) and [y] = M*[v]
|
||
|
|
// [*] = [0]
|
||
|
|
// [w] [1]
|
||
|
|
//
|
||
|
|
// x(u,v) = m00*u + m01*v + m03
|
||
|
|
// y(u,v) = m10*u + m11*v + m13
|
||
|
|
// w(u,v) = m30*u + m31*v + m33
|
||
|
|
//
|
||
|
|
// dx/du = m00, dx/dv = m01,
|
||
|
|
// dy/du = m10, dy/dv = m11
|
||
|
|
// dw/du = m30, dw/dv = m31
|
||
|
|
//
|
||
|
|
// df/du = (dx/du*w - x*dw/du)/w^2 = (m00*w - m30*x)/w^2
|
||
|
|
// df/dv = (dx/dv*w - x*dw/dv)/w^2 = (m01*w - m31*x)/w^2
|
||
|
|
// dg/du = (dy/du*w - y*dw/du)/w^2 = (m10*w - m30*y)/w^2
|
||
|
|
// dg/dv = (dy/dv*w - y*dw/du)/w^2 = (m11*w - m31*y)/w^2
|
||
|
|
//
|
||
|
|
// Singular values of [df/du df/dv] define perspective correct minimum and maximum scale factors
|
||
|
|
// [dg/du dg/dv]
|
||
|
|
// for M evaluated at (u,v)
|
||
|
|
static float local_aa_radius(const SkM44& matrix, const SkV2& p) {
|
||
|
|
SkV4 devP = matrix.map(p.x, p.y, 0.f, 1.f);
|
||
|
|
|
||
|
|
const float dxdu = matrix.rc(0,0);
|
||
|
|
const float dxdv = matrix.rc(0,1);
|
||
|
|
const float dydu = matrix.rc(1,0);
|
||
|
|
const float dydv = matrix.rc(1,1);
|
||
|
|
const float dwdu = matrix.rc(3,0);
|
||
|
|
const float dwdv = matrix.rc(3,1);
|
||
|
|
|
||
|
|
float invW2 = 1.f / (devP.w * devP.w);
|
||
|
|
// non-persp has invW2 = 1, devP.w = 1, dwdu = 0, dwdv = 0
|
||
|
|
float dfdu = (devP.w*dxdu - devP.x*dwdu) * invW2; // non-persp -> dxdu -> m00
|
||
|
|
float dfdv = (devP.w*dxdv - devP.x*dwdv) * invW2; // non-persp -> dxdv -> m01
|
||
|
|
float dgdu = (devP.w*dydu - devP.y*dwdu) * invW2; // non-persp -> dydu -> m10
|
||
|
|
float dgdv = (devP.w*dydv - devP.y*dwdv) * invW2; // non-persp -> dydv -> m11
|
||
|
|
|
||
|
|
// no-persp, this is the singular values of [m00,m01][m10,m11], which is just the upper 2x2
|
||
|
|
// and equivalent to SkMatrix::getMinmaxScales().
|
||
|
|
auto [sv1, sv2] = singular_values(dfdu, dfdv, dgdu, dgdv);
|
||
|
|
|
||
|
|
// The minimum and maximum singular values of the above matrix represent the min and maximum
|
||
|
|
// scale factors that could be applied by the 'matrix'. So if 'p' is moved 1px locally it will
|
||
|
|
// move between [min, max]px after transformation. Thus, moving 1/min px locally will move
|
||
|
|
// between [1, max/min]px after transformation, ensuring the device-space offset exceeds the
|
||
|
|
// minimum AA offset for analytic AA.
|
||
|
|
float minScale = std::min(sv1, sv2);
|
||
|
|
return kAARadius / minScale;
|
||
|
|
}
|
||
|
|
|
||
|
|
static constexpr float kMiterScale = 1.f;
|
||
|
|
static constexpr float kBevelScale = 0.0f;
|
||
|
|
static constexpr float kRoundScale = SK_FloatSqrt2 - 1.f;
|
||
|
|
|
||
|
|
struct LocalCornerVert {
|
||
|
|
SkV2 fPosition; // In unit square that each corner is normalized to
|
||
|
|
SkV2 fNormal; // Direction that AA outset is applied in
|
||
|
|
|
||
|
|
float fStrokeScale; // Signed scale factor applied to external stroke radius, should be [-1,1]
|
||
|
|
float fMirrorScale; // Scale fPosition.yx, along with external join-scale, should be [0,1].
|
||
|
|
float fCenterWeight; // Added to external center scale, > 0 forces point to center instead.
|
||
|
|
|
||
|
|
// 'cornerMapping' is a row-major 2x2 matrix [[x y], [z w]] to flip and rotate the normalized
|
||
|
|
// positions into the local coord space.
|
||
|
|
SkV3 transform(const SkM44& m, const SkV4& cornerMapping, const SkV2& cornerPt,
|
||
|
|
const SkV2& cornerRadii, const SkV4& devCenter, float centerWeight,
|
||
|
|
float strokeRadius, float joinScale, float localAARadius) const {
|
||
|
|
const bool snapToCenter = centerWeight + fCenterWeight > 0.f;
|
||
|
|
if (snapToCenter) {
|
||
|
|
return {devCenter.x, devCenter.y, devCenter.w};
|
||
|
|
} else {
|
||
|
|
// Normalized position before any additional AA offsets
|
||
|
|
SkV2 normalizedPos = fPosition + joinScale*fMirrorScale*SkV2{fPosition.y, fPosition.x};
|
||
|
|
// scales the normalized unit corner to the actual radii of the corner, before any AA
|
||
|
|
// offsets are added.
|
||
|
|
SkV2 scale = cornerRadii + SkV2{fStrokeScale*strokeRadius, fStrokeScale*strokeRadius};
|
||
|
|
normalizedPos = scale*normalizedPos - cornerRadii;
|
||
|
|
|
||
|
|
if (fStrokeScale < 0.f) {
|
||
|
|
// An inset, which means it might cross over or might be forced to the center
|
||
|
|
SkV2 maxInset = scale - SkV2{localAARadius, localAARadius};
|
||
|
|
if (maxInset.x < 0.f || maxInset.y < 0.f) {
|
||
|
|
normalizedPos =
|
||
|
|
SkV2{std::min(maxInset.x, 0.f), std::min(maxInset.y, 0.f)}
|
||
|
|
- cornerRadii;
|
||
|
|
} else {
|
||
|
|
normalizedPos += localAARadius * fNormal;
|
||
|
|
}
|
||
|
|
} // else no normal offsetting, or device-space offsetting
|
||
|
|
|
||
|
|
SkV2 localPos =
|
||
|
|
{cornerMapping.x*normalizedPos.x + cornerMapping.y*normalizedPos.y + cornerPt.x,
|
||
|
|
cornerMapping.z*normalizedPos.x + cornerMapping.w*normalizedPos.y + cornerPt.y};
|
||
|
|
SkV4 devPos = m.map(localPos.x, localPos.y, 0.f, 1.f);
|
||
|
|
|
||
|
|
const bool deviceSpaceNormal =
|
||
|
|
fStrokeScale > 0.f && (fNormal.x > 0.f || fNormal.y > 0.f);
|
||
|
|
if (deviceSpaceNormal) {
|
||
|
|
SkV2 devNorm;
|
||
|
|
{
|
||
|
|
// To calculate a device-space normal, we use the normal matrix (A^-1)^T where
|
||
|
|
// A is CTM * T(cornerPt) * cornerMapping * scale. We inline the calculation
|
||
|
|
// of (T(cornerPt)*cornerMapping*scale)^-1^T * [nx, ny, 0, 0] = N', which means
|
||
|
|
// that CTM^-1^T * N' is equivalent to N'^T*CTM^-1, which can be calculated with
|
||
|
|
// two dot products if the CTM inverse is uploaded to the GPU.
|
||
|
|
|
||
|
|
// We add epsilon so that rectangular corners are not degenerate, and circular
|
||
|
|
// corners remain unmodified. This only slightly increases inaccuracy for
|
||
|
|
// elliptical corners.
|
||
|
|
float sx = (scale.y + SK_ScalarNearlyZero) / (scale.x + SK_ScalarNearlyZero);
|
||
|
|
// Needed to calculate intermediate W of transformed normal.
|
||
|
|
float px = cornerMapping.y*cornerPt.y - cornerMapping.w*cornerPt.x;
|
||
|
|
float py = cornerMapping.z*cornerPt.x - cornerMapping.x*cornerPt.y;
|
||
|
|
// Inverse CTM, presumably calculated once as a uniform
|
||
|
|
SkM44 inv;
|
||
|
|
SkAssertResult(m.invert(&inv));
|
||
|
|
|
||
|
|
SkV4 normX4 = { sx*cornerMapping.w*fNormal.x,
|
||
|
|
-sx*cornerMapping.y*fNormal.x,
|
||
|
|
0.f,
|
||
|
|
sx*px*fNormal.x};
|
||
|
|
SkV4 normY4 = {-cornerMapping.z*fNormal.y,
|
||
|
|
cornerMapping.x*fNormal.y,
|
||
|
|
0.f,
|
||
|
|
py*fNormal.y};
|
||
|
|
|
||
|
|
SkV2 normX = {inv.col(0).dot(normX4), inv.col(1).dot(normX4)};
|
||
|
|
SkV2 normY = {inv.col(0).dot(normY4), inv.col(1).dot(normY4)};
|
||
|
|
|
||
|
|
if (joinScale == kMiterScale && fNormal.x > 0.f && fNormal.y > 0.f) {
|
||
|
|
// normX and normY represent adjacent edges' normals, so if we normalize
|
||
|
|
// them before adding together, we'll have a vector that bisects the edge
|
||
|
|
// normals instead of a vector matching fNormal, which is what we want when
|
||
|
|
// we're at a miter corner.
|
||
|
|
normX = normX.normalize();
|
||
|
|
normY = normY.normalize();
|
||
|
|
if (normX.dot(normY) < -0.8) {
|
||
|
|
// Nearly opposite directions, so the sum could have cancellation, so
|
||
|
|
// instead bisect orthogonal vectors and flip to keep consistent
|
||
|
|
float sign = normX.cross(normY) >= 0.f ? 1.f : -1.f;
|
||
|
|
normX = sign*SkV2{-normX.y, normX.x};
|
||
|
|
normY = sign*SkV2{normY.y, -normY.x};
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
devNorm = (normX + normY).normalize();
|
||
|
|
}
|
||
|
|
|
||
|
|
// The local coordinates for a device-space AA outset are clamped to the non-outset
|
||
|
|
// point, which means we don't care about remaining in the same pre-homogenous
|
||
|
|
// divide plane. This makes it very easy to determine a homogenous coordinate that
|
||
|
|
// projects to the correct device-space position.
|
||
|
|
devPos.x += devPos.w * kAARadius * devNorm.x;
|
||
|
|
devPos.y += devPos.w * kAARadius * devNorm.y;
|
||
|
|
}
|
||
|
|
|
||
|
|
return SkV3{devPos.x, devPos.y, devPos.w};
|
||
|
|
}
|
||
|
|
}
|
||
|
|
};
|
||
|
|
|
||
|
|
static constexpr float kHR2 = SK_ScalarRoot2Over2; // "half root 2"
|
||
|
|
|
||
|
|
static constexpr LocalCornerVert kCornerTemplate[19] = {
|
||
|
|
// Stroke-scale should be -1, 0, or 1.
|
||
|
|
// Mirror-scale should be 0 or 1.
|
||
|
|
// Center-weight should be -2 to never snap to center, -1 to snap when stroke coords would
|
||
|
|
// overlap, and 0 to snap for fill-style or overlapping coords.
|
||
|
|
// Local-aa-scale should be 0 or 1.
|
||
|
|
|
||
|
|
// position, normal, stroke-scale mirror-scale center-weight
|
||
|
|
// Device-space AA outsets from outer curve
|
||
|
|
{ {0.0f, 1.0f}, { 0.0f, 1.0f}, 1.0f, 0.0f, -2.f },
|
||
|
|
{ {0.0f, 1.0f}, { 0.0f, 1.0f}, 1.0f, 1.0f, -2.f },
|
||
|
|
{ {0.0f, 1.0f}, { kHR2, kHR2}, 1.0f, 1.0f, -2.f },
|
||
|
|
{ {1.0f, 0.0f}, { kHR2, kHR2}, 1.0f, 1.0f, -2.f },
|
||
|
|
{ {1.0f, 0.0f}, { 1.0f, 0.0f}, 1.0f, 1.0f, -2.f },
|
||
|
|
{ {1.0f, 0.0f}, { 1.0f, 0.0f}, 1.0f, 0.0f, -2.f },
|
||
|
|
|
||
|
|
// Outer anchors (no local or device-space normal outset)
|
||
|
|
{ {0.0f, 1.0f}, { 0.0f, 0.0f}, 1.0f, 0.0f, -2.f },
|
||
|
|
{ {0.0f, 1.0f}, { 0.0f, 0.0f}, 1.0f, 1.0f, -2.f },
|
||
|
|
{ {1.0f, 0.0f}, { 0.0f, 0.0f}, 1.0f, 1.0f, -2.f },
|
||
|
|
{ {1.0f, 0.0f}, { 0.0f, 0.0f}, 1.0f, 0.0f, -2.f },
|
||
|
|
|
||
|
|
// Center of stroke (equivalent to outer anchors when filling)
|
||
|
|
{ {0.0f, 1.0f}, { 0.0f, 0.0f}, 0.0f, 0.0f, -2.f },
|
||
|
|
{ {0.0f, 1.0f}, { 0.0f, 0.0f}, 0.0f, 1.0f, -2.f },
|
||
|
|
{ {1.0f, 0.0f}, { 0.0f, 0.0f}, 0.0f, 1.0f, -2.f },
|
||
|
|
{ {1.0f, 0.0f}, { 0.0f, 0.0f}, 0.0f, 0.0f, -2.f },
|
||
|
|
|
||
|
|
// Inner AA insets from inner curve
|
||
|
|
{ {0.0f, 1.0f}, { 0.0f, -1.0f}, -1.0f, 0.0f, -1.f },
|
||
|
|
{ {0.5f, 0.5f}, {-kHR2, -kHR2}, -1.0f, 1.0f, -1.f },
|
||
|
|
{ {1.0f, 0.0f}, {-1.0f, 0.0f}, -1.0f, 0.0f, -1.f },
|
||
|
|
|
||
|
|
// Center filling vertices (equal to inner AA insets unless center-weight = 1)
|
||
|
|
{ {0.5f, 0.5f}, {-kHR2, -kHR2}, -1.0f, 1.0f, 0.f },
|
||
|
|
{ {1.0f, 0.0f}, {-1.0f, 0.0f}, -1.0f, 0.0f, 0.f },
|
||
|
|
};
|
||
|
|
|
||
|
|
static void compute_corner(SkV3 devPts[19], const SkM44& m, const SkV4& cornerMapping,
|
||
|
|
const SkV2& cornerPt, const SkV2& cornerRadii, const SkV4& center,
|
||
|
|
float centerWeight, float localAARadius, float strokeRadius,
|
||
|
|
SkPaint::Join join) {
|
||
|
|
float joinScale;
|
||
|
|
|
||
|
|
// TODO: checking against localAARadius can snap to rect corner unexpectedly under high skew
|
||
|
|
// because localAARadius gets so big, but would be nice to be fuzzy here.
|
||
|
|
if (cornerRadii.x <= 0.f || cornerRadii.y <= 0.f) {
|
||
|
|
// Effectively a rectangular corner
|
||
|
|
joinScale = kMiterScale; // default for rect corners
|
||
|
|
if (strokeRadius > 0.f) {
|
||
|
|
// Non-hairline strokes need to adjust the join scale factor to match style.
|
||
|
|
if (join == SkPaint::kBevel_Join) {
|
||
|
|
joinScale = kBevelScale;
|
||
|
|
} else if (join == SkPaint::kRound_Join) {
|
||
|
|
joinScale = kRoundScale;
|
||
|
|
}
|
||
|
|
}
|
||
|
|
} else {
|
||
|
|
// Rounded filled corner vertices are always positioned for a round join since the
|
||
|
|
// underlying geometry has no real tangent discontinuity.
|
||
|
|
joinScale = kRoundScale;
|
||
|
|
}
|
||
|
|
|
||
|
|
for (size_t i = 0; i < std::size(kCornerTemplate); ++i) {
|
||
|
|
devPts[i] = kCornerTemplate[i].transform(m, cornerMapping, cornerPt, cornerRadii,
|
||
|
|
center, centerWeight, strokeRadius, joinScale,
|
||
|
|
localAARadius);
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
static const uint16_t kBR = 0*std::size(kCornerTemplate);
|
||
|
|
static const uint16_t kTR = 1*std::size(kCornerTemplate);
|
||
|
|
static const uint16_t kTL = 2*std::size(kCornerTemplate);
|
||
|
|
static const uint16_t kBL = 3*std::size(kCornerTemplate);
|
||
|
|
static const size_t kVertexCount = 4*std::size(kCornerTemplate);
|
||
|
|
static void compute_vertices(SkV3 devPts[kVertexCount],
|
||
|
|
const SkM44& m,
|
||
|
|
const SkRRect& rrect,
|
||
|
|
float strokeRadius,
|
||
|
|
SkPaint::Join join) {
|
||
|
|
SkV4 devCenter = m.map(rrect.getBounds().centerX(), rrect.getBounds().centerY(), 0.f, 1.f);
|
||
|
|
|
||
|
|
float localAARadius = std::max({
|
||
|
|
local_aa_radius(m, {rrect.getBounds().fRight, rrect.getBounds().fBottom}),
|
||
|
|
local_aa_radius(m, {rrect.getBounds().fRight, rrect.getBounds().fTop}),
|
||
|
|
local_aa_radius(m, {rrect.getBounds().fLeft, rrect.getBounds().fTop}),
|
||
|
|
local_aa_radius(m, {rrect.getBounds().fLeft, rrect.getBounds().fBottom})
|
||
|
|
});
|
||
|
|
|
||
|
|
float centerWeight = 0.f; // No center snapping
|
||
|
|
if (strokeRadius < 0.f) {
|
||
|
|
// A fill, so inner vertices need to snap to the center and then adjust the stroke radius
|
||
|
|
// to 0 for later math to work out nicely.
|
||
|
|
strokeRadius = 0.f;
|
||
|
|
centerWeight = 1.f;
|
||
|
|
}
|
||
|
|
|
||
|
|
// Check if the inset amount (max stroke-radius + local-aa-radius) would interfere with the
|
||
|
|
// opposite edge's inset or interfere with the adjacent corner's curve. When this happens, snap
|
||
|
|
// all the interior vertices to the center and let the fragment shader work through it.
|
||
|
|
// TODO: Could force centerWeight = 2 for filled rects and quads for simplicity around non
|
||
|
|
// orthogonal inset overlap calculations.
|
||
|
|
float maxInset = strokeRadius + localAARadius;
|
||
|
|
if (maxInset >= rrect.width() - maxInset || // L/R stroke insets would cross over
|
||
|
|
maxInset >= rrect.height() - maxInset || // T/B stroke insets would cross over
|
||
|
|
maxInset >= rrect.width() - rrect.radii(SkRRect::kLowerLeft_Corner).fX || // X corner cross
|
||
|
|
maxInset >= rrect.width() - rrect.radii(SkRRect::kLowerRight_Corner).fX ||
|
||
|
|
maxInset >= rrect.width() - rrect.radii(SkRRect::kUpperLeft_Corner).fX ||
|
||
|
|
maxInset >= rrect.width() - rrect.radii(SkRRect::kUpperRight_Corner).fX ||
|
||
|
|
maxInset >= rrect.height() - rrect.radii(SkRRect::kLowerLeft_Corner).fY || // Y corner cross
|
||
|
|
maxInset >= rrect.height() - rrect.radii(SkRRect::kLowerRight_Corner).fY ||
|
||
|
|
maxInset >= rrect.height() - rrect.radii(SkRRect::kUpperLeft_Corner).fY ||
|
||
|
|
maxInset >= rrect.height() - rrect.radii(SkRRect::kUpperRight_Corner).fY) {
|
||
|
|
// All interior vertices need to snap to the center
|
||
|
|
centerWeight = 2.f;
|
||
|
|
}
|
||
|
|
|
||
|
|
// The normalized corner template is defined relative to the quarter circle with positive X
|
||
|
|
// and positive Y, with a counter clockwise winding (if +Y points down). This corresponds to
|
||
|
|
// the bottom-right corner.
|
||
|
|
static constexpr SkV4 kBRBasis = { 1.f, 0.f, 0.f, 1.f};
|
||
|
|
static constexpr SkV4 kTRBasis = { 0.f, 1.f, -1.f, 0.f};
|
||
|
|
static constexpr SkV4 kTLBasis = {-1.f, 0.f, 0.f, -1.f};
|
||
|
|
static constexpr SkV4 kBLBasis = { 0.f, -1.f, 1.f, 0.f};
|
||
|
|
|
||
|
|
compute_corner(devPts + kBR, m, kBRBasis,
|
||
|
|
{rrect.getBounds().fRight,
|
||
|
|
rrect.getBounds().fBottom},
|
||
|
|
{rrect.radii(SkRRect::kLowerRight_Corner).fX,
|
||
|
|
rrect.radii(SkRRect::kLowerRight_Corner).fY},
|
||
|
|
devCenter, centerWeight, localAARadius, strokeRadius, join);
|
||
|
|
compute_corner(devPts + kTR, m, kTRBasis,
|
||
|
|
{rrect.getBounds().fRight,
|
||
|
|
rrect.getBounds().fTop},
|
||
|
|
{rrect.radii(SkRRect::kUpperRight_Corner).fY,
|
||
|
|
rrect.radii(SkRRect::kUpperRight_Corner).fX},
|
||
|
|
devCenter, centerWeight, localAARadius,strokeRadius, join);
|
||
|
|
compute_corner(devPts + kTL, m, kTLBasis,
|
||
|
|
{rrect.getBounds().fLeft,
|
||
|
|
rrect.getBounds().fTop},
|
||
|
|
{rrect.radii(SkRRect::kUpperLeft_Corner).fX,
|
||
|
|
rrect.radii(SkRRect::kUpperLeft_Corner).fY},
|
||
|
|
devCenter, centerWeight, localAARadius,strokeRadius, join);
|
||
|
|
compute_corner(devPts + kBL, m, kBLBasis,
|
||
|
|
{rrect.getBounds().fLeft,
|
||
|
|
rrect.getBounds().fBottom},
|
||
|
|
{rrect.radii(SkRRect::kLowerLeft_Corner).fY,
|
||
|
|
rrect.radii(SkRRect::kLowerLeft_Corner).fX},
|
||
|
|
devCenter, centerWeight, localAARadius,strokeRadius, join);
|
||
|
|
}
|
||
|
|
|
||
|
|
// All indices
|
||
|
|
static const uint16_t kIndices[] = {
|
||
|
|
// Exterior AA ramp outset
|
||
|
|
kBR+0,kBR+6,kBR+1,kBR+7,kBR+2,kBR+8,kBR+3,kBR+8,kBR+4,kBR+9,kBR+5,kBR+9,
|
||
|
|
kTR+0,kTR+6,kTR+1,kTR+7,kTR+2,kTR+8,kTR+3,kTR+8,kTR+4,kTR+9,kTR+5,kTR+9,
|
||
|
|
kTL+0,kTL+6,kTL+1,kTL+7,kTL+2,kTL+8,kTL+3,kTL+8,kTL+4,kTL+9,kTL+5,kTL+9,
|
||
|
|
kBL+0,kBL+6,kBL+1,kBL+7,kBL+2,kBL+8,kBL+3,kBL+8,kBL+4,kBL+9,kBL+5,kBL+9,
|
||
|
|
kBR+0,kBR+6,kBR+6, // close and extra vertex to jump to next strip
|
||
|
|
// Outer to central curve
|
||
|
|
kBR+6,kBR+10,kBR+7,kBR+11,kBR+8,kBR+12,kBR+9,kBR+13,
|
||
|
|
kTR+6,kTR+10,kTR+7,kTR+11,kTR+8,kTR+12,kTR+9,kTR+13,
|
||
|
|
kTL+6,kTL+10,kTL+7,kTL+11,kTL+8,kTL+12,kTL+9,kTL+13,
|
||
|
|
kBL+6,kBL+10,kBL+7,kBL+11,kBL+8,kBL+12,kBL+9,kBL+13,
|
||
|
|
kBR+6,kBR+10,kBR+10, // close and extra vertex to jump to next strip
|
||
|
|
// Center to inner curve's insets
|
||
|
|
kBR+10,kBR+14,kBR+11,kBR+15,kBR+12,kBR+16,kBR+13,kBR+16,
|
||
|
|
kTR+10,kTR+14,kTR+11,kTR+15,kTR+12,kTR+16,kTR+13,kTR+16,
|
||
|
|
kTL+10,kTL+14,kTL+11,kTL+15,kTL+12,kTL+16,kTL+13,kTL+16,
|
||
|
|
kBL+10,kBL+14,kBL+11,kBL+15,kBL+12,kBL+16,kBL+13,kBL+16,
|
||
|
|
kBR+10,kBR+14,kBR+14, // close and extra vertex to jump to next strip
|
||
|
|
// Inner inset to center of shape
|
||
|
|
kBR+14,kBR+17,kBR+15,kBR+17,kBR+16,kBR+16,kBR+18,kTR+14,
|
||
|
|
kTR+14,kTR+17,kTR+15,kTR+17,kTR+16,kTR+16,kTR+18,kTL+14,
|
||
|
|
kTL+14,kTL+17,kTL+15,kTL+17,kTL+16,kTL+16,kTL+18,kBL+14,
|
||
|
|
kBL+14,kBL+17,kBL+15,kBL+17,kBL+16,kBL+16,kBL+18,kBR+14 // close
|
||
|
|
};
|
||
|
|
|
||
|
|
// Separated to draw with different colors (vs. duplicating vertices to change colors).
|
||
|
|
static const uint16_t kOuterCornerIndices[] = {
|
||
|
|
kBR+0, kBR+0,kBR+6,kBR+1,kBR+7,kBR+2,kBR+8,kBR+3,kBR+8,kBR+4,kBR+9,kBR+5, kBR+5,
|
||
|
|
kTR+0, kTR+0,kTR+6,kTR+1,kTR+7,kTR+2,kTR+8,kTR+3,kTR+8,kTR+4,kTR+9,kTR+5, kTR+5,
|
||
|
|
kTL+0, kTL+0,kTL+6,kTL+1,kTL+7,kTL+2,kTL+8,kTL+3,kTL+8,kTL+4,kTL+9,kTL+5, kTL+5,
|
||
|
|
kBL+0, kBL+0,kBL+6,kBL+1,kBL+7,kBL+2,kBL+8,kBL+3,kBL+8,kBL+4,kBL+9,kBL+5, kBL+5,
|
||
|
|
|
||
|
|
kBR+6, kBR+6,kBR+10,kBR+7,kBR+11,kBR+8,kBR+12,kBR+9,kBR+13, kBR+13,
|
||
|
|
kTR+6, kTR+6,kTR+10,kTR+7,kTR+11,kTR+8,kTR+12,kTR+9,kTR+13, kTR+13,
|
||
|
|
kTL+6, kTL+6,kTL+10,kTL+7,kTL+11,kTL+8,kTL+12,kTL+9,kTL+13, kTL+13,
|
||
|
|
kBL+6, kBL+6,kBL+10,kBL+7,kBL+11,kBL+8,kBL+12,kBL+9,kBL+13, kBL+13
|
||
|
|
};
|
||
|
|
|
||
|
|
static const uint16_t kInnerCornerIndices[] = {
|
||
|
|
kBR+10, kBR+10,kBR+14,kBR+11,kBR+15,kBR+12,kBR+16,kBR+13, kBR+13,
|
||
|
|
kTR+10, kTR+10,kTR+14,kTR+11,kTR+15,kTR+12,kTR+16,kTR+13, kTR+13,
|
||
|
|
kTL+10, kTL+10,kTL+14,kTL+11,kTL+15,kTL+12,kTL+16,kTL+13, kTL+13,
|
||
|
|
kBL+10, kBL+10,kBL+14,kBL+11,kBL+15,kBL+12,kBL+16,kBL+13, kBL+13,
|
||
|
|
};
|
||
|
|
|
||
|
|
static const uint16_t kInteriorIndices[] = {
|
||
|
|
kBR+14,kBR+17,kBR+15,kBR+17,kBR+16,kBR+16,kBR+18,kTR+14,
|
||
|
|
kTR+14,kTR+17,kTR+15,kTR+17,kTR+16,kTR+16,kTR+18,kTL+14,
|
||
|
|
kTL+14,kTL+17,kTL+15,kTL+17,kTL+16,kTL+16,kTL+18,kBL+14,
|
||
|
|
kBL+14,kBL+17,kBL+15,kBL+17,kBL+16,kBL+16,kBL+18,kBR+14 // close
|
||
|
|
};
|
||
|
|
|
||
|
|
// Implicit in the original mesh from the tri-strip connections between corners
|
||
|
|
static const uint16_t kEdgeIndices[] = {
|
||
|
|
kBR+5, kBR+5,kBR+9,kTR+0,kTR+6, kTR+6,
|
||
|
|
kBR+9, kBR+9,kBR+13,kTR+6,kTR+10, kTR+10,
|
||
|
|
kBR+13, kBR+13,kBR+16,kTR+10,kTR+14, kTR+14,
|
||
|
|
|
||
|
|
kTR+5, kTR+5,kTR+9,kTL+0,kTL+6, kTL+6,
|
||
|
|
kTR+9, kTR+9,kTR+13,kTL+6,kTL+10, kTL+10,
|
||
|
|
kTR+13, kTR+13,kTR+16,kTL+10,kTL+14, kTL+14,
|
||
|
|
|
||
|
|
kTL+5, kTL+5,kTL+9,kBL+0,kBL+6, kBL+6,
|
||
|
|
kTL+9, kTL+9,kTL+13,kBL+6,kBL+10, kBL+10,
|
||
|
|
kTL+13, kTL+13,kTL+16,kBL+10,kBL+14, kBL+14,
|
||
|
|
|
||
|
|
kBL+5, kBL+5,kBL+9,kBR+0,kBR+6, kBR+6,
|
||
|
|
kBL+9, kBL+9,kBL+13,kBR+6,kBR+10, kBR+10,
|
||
|
|
kBL+13, kBL+13,kBL+16,kBR+10,kBR+14, kBR+14,
|
||
|
|
};
|
||
|
|
|
||
|
|
class GraphitePrimitivesSlide : public ClickHandlerSlide {
|
||
|
|
static constexpr float kControlPointRadius = 3.f;
|
||
|
|
static constexpr float kBaseScale = 50.f;
|
||
|
|
|
||
|
|
public:
|
||
|
|
GraphitePrimitivesSlide()
|
||
|
|
: fOrigin{300.f, 300.f}
|
||
|
|
, fXAxisPoint{300.f + kBaseScale, 300.f}
|
||
|
|
, fYAxisPoint{300.f, 300.f + kBaseScale}
|
||
|
|
, fStrokeWidth{10.f}
|
||
|
|
, fJoinMode(SkPaint::kMiter_Join)
|
||
|
|
, fMode(PrimitiveMode::kFillRect) {
|
||
|
|
fName = "GraphitePrimitives";
|
||
|
|
}
|
||
|
|
|
||
|
|
void draw(SkCanvas* canvas) override {
|
||
|
|
canvas->save();
|
||
|
|
SkM44 viewMatrix = canvas->getLocalToDevice();
|
||
|
|
|
||
|
|
canvas->concat(this->basisMatrix());
|
||
|
|
|
||
|
|
SkM44 totalMatrix = canvas->getLocalToDevice();
|
||
|
|
|
||
|
|
// Base shape + style
|
||
|
|
SkRRect rrect = this->primitiveShape();
|
||
|
|
canvas->drawRRect(rrect, paint(SK_ColorBLUE, this->strokeWidth(), fJoinMode));
|
||
|
|
canvas->restore();
|
||
|
|
|
||
|
|
canvas->save();
|
||
|
|
canvas->resetMatrix();
|
||
|
|
// Draw the full mesh directly in device space
|
||
|
|
this->drawVertices(canvas, totalMatrix);
|
||
|
|
// Draw the controls in device space so we get consistent circles for the click points.
|
||
|
|
SkV4 origin = viewMatrix.map(fOrigin.x, fOrigin.y, 0.f, 1.f);
|
||
|
|
SkV4 xAxis = viewMatrix.map(fXAxisPoint.x, fXAxisPoint.y, 0.f, 1.f);
|
||
|
|
SkV4 yAxis = viewMatrix.map(fYAxisPoint.x, fYAxisPoint.y, 0.f, 1.f);
|
||
|
|
|
||
|
|
// Axes
|
||
|
|
canvas->drawLine({origin.x/origin.w, origin.y/origin.w},
|
||
|
|
{xAxis.x/xAxis.w, xAxis.y/xAxis.w}, paint(SK_ColorRED, 0.f));
|
||
|
|
canvas->drawLine({origin.x/origin.w, origin.y/origin.w},
|
||
|
|
{yAxis.x/yAxis.w, yAxis.y/yAxis.w}, paint(SK_ColorGREEN, 0.f));
|
||
|
|
|
||
|
|
// Control points
|
||
|
|
canvas->drawCircle({origin.x/origin.w, origin.y/origin.w},
|
||
|
|
kControlPointRadius, paint(SK_ColorBLACK));
|
||
|
|
canvas->drawCircle({xAxis.x/xAxis.w, xAxis.y/xAxis.w},
|
||
|
|
kControlPointRadius, paint(SK_ColorRED));
|
||
|
|
canvas->drawCircle({yAxis.x/yAxis.w, yAxis.y/yAxis.w},
|
||
|
|
kControlPointRadius, paint(SK_ColorGREEN));
|
||
|
|
canvas->restore();
|
||
|
|
}
|
||
|
|
|
||
|
|
bool onChar(SkUnichar) override;
|
||
|
|
|
||
|
|
protected:
|
||
|
|
Click* onFindClickHandler(SkScalar x, SkScalar y, skui::ModifierKey) override;
|
||
|
|
bool onClick(Click*) override;
|
||
|
|
|
||
|
|
private:
|
||
|
|
class Click;
|
||
|
|
|
||
|
|
enum class PrimitiveMode {
|
||
|
|
kFillRect,
|
||
|
|
kFillRRect,
|
||
|
|
kStrokeRect,
|
||
|
|
kStrokeRRect
|
||
|
|
};
|
||
|
|
|
||
|
|
// Computed from 3 control points. Concat with CTM to get total matrix.
|
||
|
|
SkM44 basisMatrix() const {
|
||
|
|
SkV2 xAxis = (fXAxisPoint - fOrigin) / kBaseScale;
|
||
|
|
SkV2 yAxis = (fYAxisPoint - fOrigin) / kBaseScale;
|
||
|
|
|
||
|
|
return SkM44::Cols({xAxis.x, xAxis.y, 0.f, 0.f},
|
||
|
|
{yAxis.x, yAxis.y, 0.f, 0.f},
|
||
|
|
{0.f, 0.f, 1.f, 0.f},
|
||
|
|
{fOrigin.x, fOrigin.y, 0.f, 1.f});
|
||
|
|
}
|
||
|
|
|
||
|
|
float strokeWidth() const {
|
||
|
|
if (fMode == PrimitiveMode::kFillRect || fMode == PrimitiveMode::kFillRRect) {
|
||
|
|
return -1.f;
|
||
|
|
}
|
||
|
|
return fStrokeWidth;
|
||
|
|
}
|
||
|
|
|
||
|
|
SkRRect primitiveShape() const {
|
||
|
|
static const SkRect kOuterBounds = SkRect::MakeLTRB(-kBaseScale, -kBaseScale,
|
||
|
|
kBaseScale, kBaseScale);
|
||
|
|
// Filled rounded rects can have arbitrary corners
|
||
|
|
static const SkVector kOuterRadii[4] = { { 0.25f * kBaseScale, 0.75f * kBaseScale },
|
||
|
|
{ 0.f, 0.f},
|
||
|
|
{ 0.5f * kBaseScale, 0.5f * kBaseScale },
|
||
|
|
{ 0.75f * kBaseScale, 0.25f * kBaseScale } };
|
||
|
|
// // Stroked rounded rects will only have circular corners
|
||
|
|
static const SkVector kStrokeRadii[4] = { { 0.25f * kBaseScale, 0.25f * kBaseScale },
|
||
|
|
{ 0.f, 0.f },
|
||
|
|
{ 0.5f * kBaseScale, 0.5f * kBaseScale },
|
||
|
|
{ 0.75f * kBaseScale, 0.75f * kBaseScale } };
|
||
|
|
|
||
|
|
float strokeRadius = 0.5f * fStrokeWidth;
|
||
|
|
switch(fMode) {
|
||
|
|
case PrimitiveMode::kFillRect:
|
||
|
|
return SkRRect::MakeRect(kOuterBounds.makeOutset(strokeRadius, strokeRadius));
|
||
|
|
case PrimitiveMode::kFillRRect: {
|
||
|
|
SkRRect rrect;
|
||
|
|
rrect.setRectRadii(kOuterBounds, kOuterRadii);
|
||
|
|
rrect.outset(strokeRadius, strokeRadius);
|
||
|
|
return rrect; }
|
||
|
|
case PrimitiveMode::kStrokeRect:
|
||
|
|
return SkRRect::MakeRect(kOuterBounds);
|
||
|
|
case PrimitiveMode::kStrokeRRect: {
|
||
|
|
SkRRect rrect;
|
||
|
|
rrect.setRectRadii(kOuterBounds, kStrokeRadii);
|
||
|
|
return rrect;
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
SkUNREACHABLE;
|
||
|
|
}
|
||
|
|
|
||
|
|
void drawVertices(SkCanvas* canvas, const SkM44& ctm) {
|
||
|
|
SkRRect rrect = this->primitiveShape();
|
||
|
|
float strokeRadius = 0.5f * this->strokeWidth();
|
||
|
|
|
||
|
|
SkV3 points[kVertexCount];
|
||
|
|
SkPoint vertices[kVertexCount];
|
||
|
|
compute_vertices(points, ctm, rrect, strokeRadius, fJoinMode);
|
||
|
|
// SkCanvas::drawVertices() wants SkPoint, but normally we'd let the GPU handle the
|
||
|
|
// perspective division and clipping.
|
||
|
|
for (size_t i = 0; i < kVertexCount; ++i) {
|
||
|
|
vertices[i] = SkPoint{points[i].x/points[i].z, points[i].y/points[i].z};
|
||
|
|
}
|
||
|
|
|
||
|
|
auto drawMeshSubset = [vertices, canvas](SkColor color,
|
||
|
|
const uint16_t* indices,
|
||
|
|
size_t indexCount) {
|
||
|
|
sk_sp<SkVertices> mesh = SkVertices::MakeCopy(
|
||
|
|
SkVertices::kTriangleStrip_VertexMode, kVertexCount, vertices,
|
||
|
|
nullptr, nullptr, (int) indexCount, indices);
|
||
|
|
SkPaint meshPaint;
|
||
|
|
meshPaint.setColor(color);
|
||
|
|
meshPaint.setAlphaf(0.5f);
|
||
|
|
canvas->drawVertices(mesh, SkBlendMode::kSrc, meshPaint);
|
||
|
|
};
|
||
|
|
if (fColorize) {
|
||
|
|
drawMeshSubset(SK_ColorGRAY,
|
||
|
|
kEdgeIndices,
|
||
|
|
std::size(kEdgeIndices));
|
||
|
|
drawMeshSubset(SK_ColorDKGRAY,
|
||
|
|
kInteriorIndices,
|
||
|
|
std::size(kInteriorIndices));
|
||
|
|
drawMeshSubset(SK_ColorMAGENTA,
|
||
|
|
kInnerCornerIndices,
|
||
|
|
std::size(kInnerCornerIndices));
|
||
|
|
drawMeshSubset(SK_ColorCYAN,
|
||
|
|
kOuterCornerIndices,
|
||
|
|
std::size(kOuterCornerIndices));
|
||
|
|
} else {
|
||
|
|
drawMeshSubset(SK_ColorGRAY, kIndices, std::size(kIndices));
|
||
|
|
}
|
||
|
|
|
||
|
|
// Draw the edges over the triangle strip mesh, but keep track of edges already drawn so
|
||
|
|
// that we don't oversaturate AA on edges shared by multiple triangles.
|
||
|
|
std::unordered_set<uint32_t> edges;
|
||
|
|
auto drawEdge = [&edges, vertices, canvas](uint16_t e0, uint16_t e1) {
|
||
|
|
uint32_t edgeID = (std::max(e0, e1) << 16) | std::min(e0, e1);
|
||
|
|
if (edges.find(edgeID) == edges.end()) {
|
||
|
|
edges.insert(edgeID);
|
||
|
|
if (SkScalarNearlyEqual(vertices[e0].fX, vertices[e1].fX) &&
|
||
|
|
SkScalarNearlyEqual(vertices[e0].fY, vertices[e1].fY)) {
|
||
|
|
return;
|
||
|
|
}
|
||
|
|
canvas->drawLine(vertices[e0], vertices[e1], paint(SK_ColorBLACK, 0.f));
|
||
|
|
}
|
||
|
|
};
|
||
|
|
for (size_t i = 2; i < std::size(kIndices); ++i) {
|
||
|
|
drawEdge(kIndices[i-1], kIndices[i]);
|
||
|
|
drawEdge(kIndices[i-2], kIndices[i]);
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
// This Sample is responsive to the entire transform of the viewer slide, including the
|
||
|
|
// transform (rotation, scale, and perspective) selected from the widget. The 3 points below
|
||
|
|
// define the location and basis of the local coordinate space, relative to the viewer's
|
||
|
|
// coordinate space. This is used instead of the root canvas coordinate space because it aligns
|
||
|
|
// with the coordinate space that the click handler operates in.
|
||
|
|
SkV2 fOrigin;
|
||
|
|
SkV2 fXAxisPoint;
|
||
|
|
SkV2 fYAxisPoint;
|
||
|
|
|
||
|
|
float fStrokeWidth;
|
||
|
|
SkPaint::Join fJoinMode;
|
||
|
|
PrimitiveMode fMode;
|
||
|
|
bool fColorize = true;
|
||
|
|
};
|
||
|
|
|
||
|
|
class GraphitePrimitivesSlide::Click : public ClickHandlerSlide::Click {
|
||
|
|
public:
|
||
|
|
Click(SkV2* point) : fPoint(point) {}
|
||
|
|
|
||
|
|
void drag() {
|
||
|
|
SkVector delta = fCurr - fPrev;
|
||
|
|
*fPoint += {delta.fX, delta.fY};
|
||
|
|
}
|
||
|
|
|
||
|
|
private:
|
||
|
|
SkV2* fPoint;
|
||
|
|
};
|
||
|
|
|
||
|
|
ClickHandlerSlide::Click* GraphitePrimitivesSlide::onFindClickHandler(SkScalar x, SkScalar y,
|
||
|
|
skui::ModifierKey) {
|
||
|
|
auto selected = [x,y](const SkV2& p) {
|
||
|
|
return ((p - SkV2{x,y}).length() < kControlPointRadius);
|
||
|
|
};
|
||
|
|
|
||
|
|
if (selected(fOrigin)) {
|
||
|
|
return new Click(&fOrigin);
|
||
|
|
} else if (selected(fXAxisPoint)) {
|
||
|
|
return new Click(&fXAxisPoint);
|
||
|
|
} else if (selected(fYAxisPoint)) {
|
||
|
|
return new Click(&fYAxisPoint);
|
||
|
|
} else {
|
||
|
|
return nullptr;
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
bool GraphitePrimitivesSlide::onClick(ClickHandlerSlide::Click* click) {
|
||
|
|
Click* myClick = (Click*) click;
|
||
|
|
myClick->drag();
|
||
|
|
return true;
|
||
|
|
}
|
||
|
|
|
||
|
|
bool GraphitePrimitivesSlide::onChar(SkUnichar code) {
|
||
|
|
switch(code) {
|
||
|
|
case '1':
|
||
|
|
fMode = PrimitiveMode::kFillRect;
|
||
|
|
return true;
|
||
|
|
case '2':
|
||
|
|
fMode = PrimitiveMode::kFillRRect;
|
||
|
|
return true;
|
||
|
|
case '3':
|
||
|
|
fMode = PrimitiveMode::kStrokeRect;
|
||
|
|
return true;
|
||
|
|
case '4':
|
||
|
|
fMode = PrimitiveMode::kStrokeRRect;
|
||
|
|
return true;
|
||
|
|
case '-':
|
||
|
|
fStrokeWidth = std::max(0.f, fStrokeWidth - 0.4f);
|
||
|
|
return true;
|
||
|
|
case '=':
|
||
|
|
fStrokeWidth = std::min(5 * kBaseScale, fStrokeWidth + 0.4f);
|
||
|
|
return true;
|
||
|
|
case 'q':
|
||
|
|
fJoinMode = SkPaint::kRound_Join;
|
||
|
|
return true;
|
||
|
|
case 'w':
|
||
|
|
fJoinMode = SkPaint::kBevel_Join;
|
||
|
|
return true;
|
||
|
|
case 'e':
|
||
|
|
fJoinMode = SkPaint::kMiter_Join;
|
||
|
|
return true;
|
||
|
|
case 'r':
|
||
|
|
fStrokeWidth = 10.f;
|
||
|
|
fOrigin = {300.f, 300.f};
|
||
|
|
fXAxisPoint = {300.f + kBaseScale, 300.f};
|
||
|
|
fYAxisPoint = {300.f, 300.f + kBaseScale};
|
||
|
|
return true;
|
||
|
|
case 'c':
|
||
|
|
fColorize = !fColorize;
|
||
|
|
return true;
|
||
|
|
}
|
||
|
|
return false;
|
||
|
|
}
|
||
|
|
|
||
|
|
DEF_SLIDE(return new GraphitePrimitivesSlide();)
|