/* * Copyright (c) 2020 Samsung Electronics Co., Ltd. All rights reserved. * Permission is hereby granted, free of charge, to any person obtaining a copy * of this software and associated documentation files (the "Software"), to deal * in the Software without restriction, including without limitation the rights * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell * copies of the Software, and to permit persons to whom the Software is * furnished to do so, subject to the following conditions: * The above copyright notice and this permission notice shall be included in all * copies or substantial portions of the Software. * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE * SOFTWARE. */ #include "vpath.h" #include #include #include #include "vbezier.h" #include "vdebug.h" #include "vline.h" #include "vrect.h" V_BEGIN_NAMESPACE void VPath::VPathData::transform(const VMatrix &m) { for (auto &i : m_points) { i = m.map(i); } mLengthDirty = true; } float VPath::VPathData::length() const { if (!mLengthDirty) return mLength; mLengthDirty = false; mLength = 0.0; size_t i = 0; for (auto e : m_elements) { switch (e) { case VPath::Element::MoveTo: i++; break; case VPath::Element::LineTo: { mLength += VLine(m_points[i - 1], m_points[i]).length(); i++; break; } case VPath::Element::CubicTo: { mLength += VBezier::fromPoints(m_points[i - 1], m_points[i], m_points[i + 1], m_points[i + 2]) .length(); i += 3; break; } case VPath::Element::Close: break; } } return mLength; } void VPath::VPathData::checkNewSegment() { if (mNewSegment) { moveTo(0, 0); mNewSegment = false; } } void VPath::VPathData::moveTo(float x, float y) { mStartPoint = {x, y}; mNewSegment = false; m_elements.emplace_back(VPath::Element::MoveTo); m_points.emplace_back(x, y); m_segments++; mLengthDirty = true; } void VPath::VPathData::lineTo(float x, float y) { checkNewSegment(); m_elements.emplace_back(VPath::Element::LineTo); m_points.emplace_back(x, y); mLengthDirty = true; } void VPath::VPathData::cubicTo(float cx1, float cy1, float cx2, float cy2, float ex, float ey) { checkNewSegment(); m_elements.emplace_back(VPath::Element::CubicTo); m_points.emplace_back(cx1, cy1); m_points.emplace_back(cx2, cy2); m_points.emplace_back(ex, ey); mLengthDirty = true; } void VPath::VPathData::close() { if (empty()) return; const VPointF &lastPt = m_points.back(); if (!fuzzyCompare(mStartPoint, lastPt)) { lineTo(mStartPoint.x(), mStartPoint.y()); } m_elements.push_back(VPath::Element::Close); mNewSegment = true; mLengthDirty = true; } void VPath::VPathData::reset() { if (empty()) return; m_elements.clear(); m_points.clear(); m_segments = 0; mLength = 0; mLengthDirty = false; } size_t VPath::VPathData::segments() const { return m_segments; } void VPath::VPathData::reserve(size_t pts, size_t elms) { if (m_points.capacity() < m_points.size() + pts) m_points.reserve(m_points.size() + pts); if (m_elements.capacity() < m_elements.size() + elms) m_elements.reserve(m_elements.size() + elms); } static VPointF curvesForArc(const VRectF &, float, float, VPointF *, size_t *); static constexpr float PATH_KAPPA = 0.5522847498f; static constexpr float K_PI = 3.141592f; void VPath::VPathData::arcTo(const VRectF &rect, float startAngle, float sweepLength, bool forceMoveTo) { size_t point_count = 0; VPointF pts[15]; VPointF curve_start = curvesForArc(rect, startAngle, sweepLength, pts, &point_count); reserve(point_count + 1, point_count / 3 + 1); if (empty() || forceMoveTo) { moveTo(curve_start.x(), curve_start.y()); } else { lineTo(curve_start.x(), curve_start.y()); } for (size_t i = 0; i < point_count; i += 3) { cubicTo(pts[i].x(), pts[i].y(), pts[i + 1].x(), pts[i + 1].y(), pts[i + 2].x(), pts[i + 2].y()); } } void VPath::VPathData::addCircle(float cx, float cy, float radius, VPath::Direction dir) { addOval(VRectF(cx - radius, cy - radius, 2 * radius, 2 * radius), dir); } void VPath::VPathData::addOval(const VRectF &rect, VPath::Direction dir) { if (rect.empty()) return; float x = rect.x(); float y = rect.y(); float w = rect.width(); float w2 = rect.width() / 2; float w2k = w2 * PATH_KAPPA; float h = rect.height(); float h2 = rect.height() / 2; float h2k = h2 * PATH_KAPPA; reserve(13, 6); // 1Move + 4Cubic + 1Close if (dir == VPath::Direction::CW) { // moveto 12 o'clock. moveTo(x + w2, y); // 12 -> 3 o'clock cubicTo(x + w2 + w2k, y, x + w, y + h2 - h2k, x + w, y + h2); // 3 -> 6 o'clock cubicTo(x + w, y + h2 + h2k, x + w2 + w2k, y + h, x + w2, y + h); // 6 -> 9 o'clock cubicTo(x + w2 - w2k, y + h, x, y + h2 + h2k, x, y + h2); // 9 -> 12 o'clock cubicTo(x, y + h2 - h2k, x + w2 - w2k, y, x + w2, y); } else { // moveto 12 o'clock. moveTo(x + w2, y); // 12 -> 9 o'clock cubicTo(x + w2 - w2k, y, x, y + h2 - h2k, x, y + h2); // 9 -> 6 o'clock cubicTo(x, y + h2 + h2k, x + w2 - w2k, y + h, x + w2, y + h); // 6 -> 3 o'clock cubicTo(x + w2 + w2k, y + h, x + w, y + h2 + h2k, x + w, y + h2); // 3 -> 12 o'clock cubicTo(x + w, y + h2 - h2k, x + w2 + w2k, y, x + w2, y); } close(); } void VPath::VPathData::addRect(const VRectF &rect, VPath::Direction dir) { float x = rect.x(); float y = rect.y(); float w = rect.width(); float h = rect.height(); if (vCompare(w, 0.f) && vCompare(h, 0.f)) return; reserve(5, 6); // 1Move + 4Line + 1Close if (dir == VPath::Direction::CW) { moveTo(x + w, y); lineTo(x + w, y + h); lineTo(x, y + h); lineTo(x, y); close(); } else { moveTo(x + w, y); lineTo(x, y); lineTo(x, y + h); lineTo(x + w, y + h); close(); } } void VPath::VPathData::addRoundRect(const VRectF &rect, float roundness, VPath::Direction dir) { if (2 * roundness > rect.width()) roundness = rect.width() / 2.0f; if (2 * roundness > rect.height()) roundness = rect.height() / 2.0f; addRoundRect(rect, roundness, roundness, dir); } void VPath::VPathData::addRoundRect(const VRectF &rect, float rx, float ry, VPath::Direction dir) { if (vCompare(rx, 0.f) || vCompare(ry, 0.f)) { addRect(rect, dir); return; } float x = rect.x(); float y = rect.y(); float w = rect.width(); float h = rect.height(); // clamp the rx and ry radius value. rx = 2 * rx; ry = 2 * ry; if (rx > w) rx = w; if (ry > h) ry = h; reserve(17, 10); // 1Move + 4Cubic + 1Close if (dir == VPath::Direction::CW) { moveTo(x + w, y + ry / 2.f); arcTo(VRectF(x + w - rx, y + h - ry, rx, ry), 0, -90, false); arcTo(VRectF(x, y + h - ry, rx, ry), -90, -90, false); arcTo(VRectF(x, y, rx, ry), -180, -90, false); arcTo(VRectF(x + w - rx, y, rx, ry), -270, -90, false); close(); } else { moveTo(x + w, y + ry / 2.f); arcTo(VRectF(x + w - rx, y, rx, ry), 0, 90, false); arcTo(VRectF(x, y, rx, ry), 90, 90, false); arcTo(VRectF(x, y + h - ry, rx, ry), 180, 90, false); arcTo(VRectF(x + w - rx, y + h - ry, rx, ry), 270, 90, false); close(); } } static float tForArcAngle(float angle); void findEllipseCoords(const VRectF &r, float angle, float length, VPointF *startPoint, VPointF *endPoint) { if (r.empty()) { if (startPoint) *startPoint = VPointF(); if (endPoint) *endPoint = VPointF(); return; } float w2 = r.width() / 2; float h2 = r.height() / 2; float angles[2] = {angle, angle + length}; VPointF *points[2] = {startPoint, endPoint}; for (int i = 0; i < 2; ++i) { if (!points[i]) continue; float theta = angles[i] - 360 * floorf(angles[i] / 360); float t = theta / 90; // truncate int quadrant = int(t); t -= quadrant; t = tForArcAngle(90 * t); // swap x and y? if (quadrant & 1) t = 1 - t; float a, b, c, d; VBezier::coefficients(t, a, b, c, d); VPointF p(a + b + c * PATH_KAPPA, d + c + b * PATH_KAPPA); // left quadrants if (quadrant == 1 || quadrant == 2) p.rx() = -p.x(); // top quadrants if (quadrant == 0 || quadrant == 1) p.ry() = -p.y(); *points[i] = r.center() + VPointF(w2 * p.x(), h2 * p.y()); } } static float tForArcAngle(float angle) { float radians, cos_angle, sin_angle, tc, ts, t; if (vCompare(angle, 0.f)) return 0; if (vCompare(angle, 90.0f)) return 1; radians = (angle / 180) * K_PI; cos_angle = cosf(radians); sin_angle = sinf(radians); // initial guess tc = angle / 90; // do some iterations of newton's method to approximate cos_angle // finds the zero of the function b.pointAt(tc).x() - cos_angle tc -= ((((2 - 3 * PATH_KAPPA) * tc + 3 * (PATH_KAPPA - 1)) * tc) * tc + 1 - cos_angle) // value / (((6 - 9 * PATH_KAPPA) * tc + 6 * (PATH_KAPPA - 1)) * tc); // derivative tc -= ((((2 - 3 * PATH_KAPPA) * tc + 3 * (PATH_KAPPA - 1)) * tc) * tc + 1 - cos_angle) // value / (((6 - 9 * PATH_KAPPA) * tc + 6 * (PATH_KAPPA - 1)) * tc); // derivative // initial guess ts = tc; // do some iterations of newton's method to approximate sin_angle // finds the zero of the function b.pointAt(tc).y() - sin_angle ts -= ((((3 * PATH_KAPPA - 2) * ts - 6 * PATH_KAPPA + 3) * ts + 3 * PATH_KAPPA) * ts - sin_angle) / (((9 * PATH_KAPPA - 6) * ts + 12 * PATH_KAPPA - 6) * ts + 3 * PATH_KAPPA); ts -= ((((3 * PATH_KAPPA - 2) * ts - 6 * PATH_KAPPA + 3) * ts + 3 * PATH_KAPPA) * ts - sin_angle) / (((9 * PATH_KAPPA - 6) * ts + 12 * PATH_KAPPA - 6) * ts + 3 * PATH_KAPPA); // use the average of the t that best approximates cos_angle // and the t that best approximates sin_angle t = 0.5f * (tc + ts); return t; } // The return value is the starting point of the arc static VPointF curvesForArc(const VRectF &rect, float startAngle, float sweepLength, VPointF *curves, size_t *point_count) { if (rect.empty()) { return {}; } float x = rect.x(); float y = rect.y(); float w = rect.width(); float w2 = rect.width() / 2; float w2k = w2 * PATH_KAPPA; float h = rect.height(); float h2 = rect.height() / 2; float h2k = h2 * PATH_KAPPA; VPointF points[16] = { // start point VPointF(x + w, y + h2), // 0 -> 270 degrees VPointF(x + w, y + h2 + h2k), VPointF(x + w2 + w2k, y + h), VPointF(x + w2, y + h), // 270 -> 180 degrees VPointF(x + w2 - w2k, y + h), VPointF(x, y + h2 + h2k), VPointF(x, y + h2), // 180 -> 90 degrees VPointF(x, y + h2 - h2k), VPointF(x + w2 - w2k, y), VPointF(x + w2, y), // 90 -> 0 degrees VPointF(x + w2 + w2k, y), VPointF(x + w, y + h2 - h2k), VPointF(x + w, y + h2)}; if (sweepLength > 360) sweepLength = 360; else if (sweepLength < -360) sweepLength = -360; // Special case fast paths if (startAngle == 0.0f) { if (vCompare(sweepLength, 360)) { for (int i = 11; i >= 0; --i) curves[(*point_count)++] = points[i]; return points[12]; } else if (vCompare(sweepLength, -360)) { for (int i = 1; i <= 12; ++i) curves[(*point_count)++] = points[i]; return points[0]; } } int startSegment = int(floorf(startAngle / 90.0f)); int endSegment = int(floorf((startAngle + sweepLength) / 90.0f)); float startT = (startAngle - startSegment * 90) / 90; float endT = (startAngle + sweepLength - endSegment * 90) / 90; int delta = sweepLength > 0 ? 1 : -1; if (delta < 0) { startT = 1 - startT; endT = 1 - endT; } // avoid empty start segment if (vIsZero(startT - float(1))) { startT = 0; startSegment += delta; } // avoid empty end segment if (vIsZero(endT)) { endT = 1; endSegment -= delta; } startT = tForArcAngle(startT * 90); endT = tForArcAngle(endT * 90); const bool splitAtStart = !vIsZero(startT); const bool splitAtEnd = !vIsZero(endT - float(1)); const int end = endSegment + delta; // empty arc? if (startSegment == end) { const int quadrant = 3 - ((startSegment % 4) + 4) % 4; const int j = 3 * quadrant; return delta > 0 ? points[j + 3] : points[j]; } VPointF startPoint, endPoint; findEllipseCoords(rect, startAngle, sweepLength, &startPoint, &endPoint); for (int i = startSegment; i != end; i += delta) { const int quadrant = 3 - ((i % 4) + 4) % 4; const int j = 3 * quadrant; VBezier b; if (delta > 0) b = VBezier::fromPoints(points[j + 3], points[j + 2], points[j + 1], points[j]); else b = VBezier::fromPoints(points[j], points[j + 1], points[j + 2], points[j + 3]); // empty arc? if (startSegment == endSegment && vCompare(startT, endT)) return startPoint; if (i == startSegment) { if (i == endSegment && splitAtEnd) b = b.onInterval(startT, endT); else if (splitAtStart) b = b.onInterval(startT, 1); } else if (i == endSegment && splitAtEnd) { b = b.onInterval(0, endT); } // push control points curves[(*point_count)++] = b.pt2(); curves[(*point_count)++] = b.pt3(); curves[(*point_count)++] = b.pt4(); } curves[*(point_count)-1] = endPoint; return startPoint; } void VPath::VPathData::addPolystar(float points, float innerRadius, float outerRadius, float innerRoundness, float outerRoundness, float startAngle, float cx, float cy, VPath::Direction dir) { const static float POLYSTAR_MAGIC_NUMBER = 0.47829f / 0.28f; float currentAngle = (startAngle - 90.0f) * K_PI / 180.0f; float x; float y; float partialPointRadius = 0; float anglePerPoint = (2.0f * K_PI / points); float halfAnglePerPoint = anglePerPoint / 2.0f; float partialPointAmount = points - floorf(points); bool longSegment = false; size_t numPoints = size_t(ceilf(points) * 2); float angleDir = ((dir == VPath::Direction::CW) ? 1.0f : -1.0f); bool hasRoundness = false; innerRoundness /= 100.0f; outerRoundness /= 100.0f; if (!vCompare(partialPointAmount, 0)) { currentAngle += halfAnglePerPoint * (1.0f - partialPointAmount) * angleDir; } if (!vCompare(partialPointAmount, 0)) { partialPointRadius = innerRadius + partialPointAmount * (outerRadius - innerRadius); x = partialPointRadius * cosf(currentAngle); y = partialPointRadius * sinf(currentAngle); currentAngle += anglePerPoint * partialPointAmount / 2.0f * angleDir; } else { x = outerRadius * cosf(currentAngle); y = outerRadius * sinf(currentAngle); currentAngle += halfAnglePerPoint * angleDir; } if (vIsZero(innerRoundness) && vIsZero(outerRoundness)) { reserve(numPoints + 2, numPoints + 3); } else { reserve(numPoints * 3 + 2, numPoints + 3); hasRoundness = true; } moveTo(x + cx, y + cy); for (size_t i = 0; i < numPoints; i++) { float radius = longSegment ? outerRadius : innerRadius; float dTheta = halfAnglePerPoint; if (!vIsZero(partialPointRadius) && i == numPoints - 2) { dTheta = anglePerPoint * partialPointAmount / 2.0f; } if (!vIsZero(partialPointRadius) && i == numPoints - 1) { radius = partialPointRadius; } float previousX = x; float previousY = y; x = radius * cosf(currentAngle); y = radius * sinf(currentAngle); if (hasRoundness) { float cp1Theta = (atan2f(previousY, previousX) - K_PI / 2.0f * angleDir); float cp1Dx = cosf(cp1Theta); float cp1Dy = sinf(cp1Theta); float cp2Theta = (atan2f(y, x) - K_PI / 2.0f * angleDir); float cp2Dx = cosf(cp2Theta); float cp2Dy = sinf(cp2Theta); float cp1Roundness = longSegment ? innerRoundness : outerRoundness; float cp2Roundness = longSegment ? outerRoundness : innerRoundness; float cp1Radius = longSegment ? innerRadius : outerRadius; float cp2Radius = longSegment ? outerRadius : innerRadius; float cp1x = cp1Radius * cp1Roundness * POLYSTAR_MAGIC_NUMBER * cp1Dx / points; float cp1y = cp1Radius * cp1Roundness * POLYSTAR_MAGIC_NUMBER * cp1Dy / points; float cp2x = cp2Radius * cp2Roundness * POLYSTAR_MAGIC_NUMBER * cp2Dx / points; float cp2y = cp2Radius * cp2Roundness * POLYSTAR_MAGIC_NUMBER * cp2Dy / points; if (!vIsZero(partialPointAmount) && ((i == 0) || (i == numPoints - 1))) { cp1x *= partialPointAmount; cp1y *= partialPointAmount; cp2x *= partialPointAmount; cp2y *= partialPointAmount; } cubicTo(previousX - cp1x + cx, previousY - cp1y + cy, x + cp2x + cx, y + cp2y + cy, x + cx, y + cy); } else { lineTo(x + cx, y + cy); } currentAngle += dTheta * angleDir; longSegment = !longSegment; } close(); } void VPath::VPathData::addPolygon(float points, float radius, float roundness, float startAngle, float cx, float cy, VPath::Direction dir) { // TODO: Need to support floating point number for number of points const static float POLYGON_MAGIC_NUMBER = 0.25; float currentAngle = (startAngle - 90.0f) * K_PI / 180.0f; float x; float y; float anglePerPoint = 2.0f * K_PI / floorf(points); size_t numPoints = size_t(floorf(points)); float angleDir = ((dir == VPath::Direction::CW) ? 1.0f : -1.0f); bool hasRoundness = false; roundness /= 100.0f; currentAngle = (currentAngle - 90.0f) * K_PI / 180.0f; x = radius * cosf(currentAngle); y = radius * sinf(currentAngle); currentAngle += anglePerPoint * angleDir; if (vIsZero(roundness)) { reserve(numPoints + 2, numPoints + 3); } else { reserve(numPoints * 3 + 2, numPoints + 3); hasRoundness = true; } moveTo(x + cx, y + cy); for (size_t i = 0; i < numPoints; i++) { float previousX = x; float previousY = y; x = (radius * cosf(currentAngle)); y = (radius * sinf(currentAngle)); if (hasRoundness) { float cp1Theta = (atan2f(previousY, previousX) - K_PI / 2.0f * angleDir); float cp1Dx = cosf(cp1Theta); float cp1Dy = sinf(cp1Theta); float cp2Theta = atan2f(y, x) - K_PI / 2.0f * angleDir; float cp2Dx = cosf(cp2Theta); float cp2Dy = sinf(cp2Theta); float cp1x = radius * roundness * POLYGON_MAGIC_NUMBER * cp1Dx; float cp1y = radius * roundness * POLYGON_MAGIC_NUMBER * cp1Dy; float cp2x = radius * roundness * POLYGON_MAGIC_NUMBER * cp2Dx; float cp2y = radius * roundness * POLYGON_MAGIC_NUMBER * cp2Dy; cubicTo(previousX - cp1x + cx, previousY - cp1y + cy, x + cp2x + cx, y + cp2y + cy, x, y); } else { lineTo(x + cx, y + cy); } currentAngle += anglePerPoint * angleDir; } close(); } void VPath::VPathData::addPath(const VPathData &path, const VMatrix *m) { size_t segment = path.segments(); // make sure enough memory available if (m_points.capacity() < m_points.size() + path.m_points.size()) m_points.reserve(m_points.size() + path.m_points.size()); if (m_elements.capacity() < m_elements.size() + path.m_elements.size()) m_elements.reserve(m_elements.size() + path.m_elements.size()); if (m) { for (const auto &i : path.m_points) { m_points.push_back(m->map(i)); } } else { std::copy(path.m_points.begin(), path.m_points.end(), back_inserter(m_points)); } std::copy(path.m_elements.begin(), path.m_elements.end(), back_inserter(m_elements)); m_segments += segment; mLengthDirty = true; } V_END_NAMESPACE