C++ / fmt (pull_request) Failing after 5s
C++ / build (pull_request) Failing after 11s
C++ / clang-tidy (pull_request) Failing after 10s
JS/TS / fmt (pull_request) Successful in 12s
JS/TS / lint (pull_request) Successful in 10s
Python / static (pull_request) Failing after 27s
Rust / build (pull_request) Successful in 54s
Python / verify (pull_request) Failing after 1m26s
Rust / fmt (pull_request) Successful in 46s
Rust / clippy (pull_request) Successful in 1m6s
204 lines
7.1 KiB
C++
204 lines
7.1 KiB
C++
#include "wavylinerenderer.hpp"
|
|
#include "wavyline.hpp"
|
|
#include <qcanvaspainter.h>
|
|
#include <algorithm>
|
|
#include <cmath>
|
|
|
|
namespace ZShell::controls {
|
|
|
|
void WavyLineRenderer::synchronizeData(QCanvasPainterItem* item) {
|
|
auto* wavyLine = static_cast<WavyLine*>(item);
|
|
|
|
m_lineWidth = wavyLine->m_lineWidth;
|
|
m_amplitudeMultiplier = wavyLine->m_amplitudeMultiplier;
|
|
m_frequency = wavyLine->m_frequency;
|
|
m_startX = wavyLine->m_startX;
|
|
m_fullLength = wavyLine->m_fullLength;
|
|
m_color = wavyLine->m_color;
|
|
m_waveProgress = wavyLine->m_waveProgress;
|
|
m_pathType = wavyLine->m_pathType;
|
|
m_startAngle = wavyLine->m_startAngle;
|
|
m_fullAngle = wavyLine->m_fullAngle;
|
|
m_radius = wavyLine->m_radius;
|
|
m_value = wavyLine->m_value;
|
|
m_dpr = wavyLine->m_dpr > 0.0 ? wavyLine->m_dpr : 1.0;
|
|
}
|
|
|
|
namespace {
|
|
qint64 toDev(qreal logical, qreal dpr) {
|
|
return std::llround(logical * dpr);
|
|
}
|
|
|
|
qreal fromDev(qint64 device, qreal dpr) {
|
|
return qreal(device) / dpr;
|
|
}
|
|
|
|
// A center coordinate whose device position shares the parity of the stroke's
|
|
// device width: an even width centered on a pixel boundary and an odd width
|
|
// centered on a pixel center are the two cases where both edges of the stroke
|
|
// land on whole device pixels. Floored, so the caller gets the same value from
|
|
// the same device size every time; a rounded half could flip between two grid
|
|
// points depending on floating point noise.
|
|
qreal snapCenterDev(qint64 sizeDev, qint64 widthDev) {
|
|
const double halfParity = (widthDev % 2) == 0 ? 0.0 : 0.5;
|
|
return std::floor(static_cast<double>(sizeDev) / 2.0 - halfParity)
|
|
+ halfParity;
|
|
}
|
|
|
|
qint64 snapAmplitude(qint64 widthDev, qreal multiplier) {
|
|
return std::llround(static_cast<double>(widthDev) * multiplier);
|
|
}
|
|
} // namespace
|
|
|
|
void WavyLineRenderer::paint(QCanvasPainter* painter) {
|
|
painter->clearRect(0, 0, width(), height());
|
|
|
|
// The stroke is quantized in the item's own device pixel space, never
|
|
// against its position: the parent layout already places the item on the
|
|
// device grid, so a local whole-device value stays whole-device wherever
|
|
// the item moves, and a move cannot change the painted path.
|
|
const qreal dpr = m_dpr;
|
|
m_lineWidthDev = (std::max)(qint64(1), toDev(m_lineWidth, dpr));
|
|
m_amplitudeDev = snapAmplitude(m_lineWidthDev, m_amplitudeMultiplier);
|
|
|
|
painter->setStrokeStyle(m_color);
|
|
painter->setLineWidth(fromDev(m_lineWidthDev, dpr));
|
|
painter->setLineCap(QCanvasPainter::LineCap::Round);
|
|
|
|
if (m_pathType == WavyLine::Arc) {
|
|
paintArc(painter);
|
|
} else {
|
|
paintLinear(painter);
|
|
}
|
|
}
|
|
|
|
void WavyLineRenderer::paintLinear(QCanvasPainter* painter) {
|
|
const qreal dpr = m_dpr;
|
|
const auto lineWidth = fromDev(m_lineWidthDev, dpr);
|
|
const auto phase = m_waveProgress * 2 * M_PI;
|
|
const qint64 heightDev = toDev(height(), dpr);
|
|
const qreal cyDev = snapCenterDev(heightDev, m_lineWidthDev);
|
|
const auto centerY = cyDev / dpr;
|
|
const auto len = m_fullLength > 0 ? m_fullLength : 1;
|
|
const auto start = lineWidth / 2.0;
|
|
const auto fullEnd = width() - lineWidth / 2.0;
|
|
const auto drawEnd = start + (fullEnd - start) * m_value;
|
|
|
|
// Keep the crest inside the item's own texture, with one device pixel for
|
|
// the antialiasing feather, so a tight item cannot cut it flat.
|
|
const qint64 halfDev = (m_lineWidthDev + 1) / 2;
|
|
const qint64 roomDev = static_cast<qint64>(
|
|
std::floor((std::min)(cyDev, qreal(heightDev) - cyDev)));
|
|
const qint64 amplitudeDev = (std::max)(qint64(0), roomDev - halfDev - 1);
|
|
const auto amplitude =
|
|
fromDev((std::min)(m_amplitudeDev, amplitudeDev), dpr);
|
|
|
|
painter->beginPath();
|
|
|
|
bool first = true;
|
|
// One vertex per device pixel column. A segment spanning several columns
|
|
// interpolates its edge across all of them, which is what makes a sloped
|
|
// stroke look soft rather than crisply anti-aliased.
|
|
const qint64 firstCol = toDev(start, dpr);
|
|
const qint64 lastCol = toDev(drawEnd, dpr);
|
|
for (qint64 col = firstCol; col <= lastCol; ++col) {
|
|
const auto x = fromDev(col, dpr);
|
|
const auto theta =
|
|
m_frequency * 2 * M_PI * (x + m_startX) / len + phase;
|
|
const auto waveY = centerY + amplitude * qSin(theta);
|
|
if (first) {
|
|
painter->moveTo(x, waveY);
|
|
first = false;
|
|
} else {
|
|
painter->lineTo(x, waveY);
|
|
}
|
|
}
|
|
|
|
painter->stroke();
|
|
}
|
|
|
|
void WavyLineRenderer::paintArc(QCanvasPainter* painter) {
|
|
if (m_fullAngle <= 0) {
|
|
return;
|
|
}
|
|
|
|
const qreal dpr = m_dpr;
|
|
const auto amplitude = fromDev(m_amplitudeDev, dpr);
|
|
const auto startAngleRad = m_startAngle * M_PI / 180.0;
|
|
const auto fullAngleRad = m_fullAngle * M_PI / 180.0;
|
|
|
|
// The center comes from the item's own device size and the stroke parity,
|
|
// the same rule Components/CircularProgress.qml applies to its background
|
|
// Shape arc, so the two rings share one center.
|
|
const qint64 widthDev = toDev(width(), dpr);
|
|
const qint64 heightDev = toDev(height(), dpr);
|
|
const qreal cxDev = snapCenterDev(widthDev, m_lineWidthDev);
|
|
const qreal cyDev = snapCenterDev(heightDev, m_lineWidthDev);
|
|
const auto cx = cxDev / dpr;
|
|
const auto cy = cyDev / dpr;
|
|
|
|
// The path is painted into a texture exactly the item's size, so anything
|
|
// past its edge is cut flat. Keep the outer stroke edge, plus the wave
|
|
// crest, inside that rect and leave one device pixel for the antialiasing
|
|
// feather. Derived from the item's own size and its local center, so it
|
|
// does not depend on where the item is placed.
|
|
const qint64 halfDev = (m_lineWidthDev + 1) / 2;
|
|
const qint64 roomDev = static_cast<qint64>(
|
|
std::floor((std::min)((std::min)(cxDev, qreal(widthDev) - cxDev),
|
|
(std::min)(cyDev, qreal(heightDev) - cyDev))));
|
|
const qint64 maxRadiusDev = roomDev - m_amplitudeDev - halfDev - 1;
|
|
|
|
// The radius is a whole device pixel count. Derived from the item's own
|
|
// device size, so it does not depend on where the item is.
|
|
qint64 radiusDev;
|
|
if (m_radius > 0) {
|
|
radiusDev = toDev(m_radius, dpr);
|
|
} else {
|
|
const qint64 inner = (std::min)(widthDev, heightDev) - m_lineWidthDev -
|
|
2 * m_amplitudeDev;
|
|
radiusDev = (std::max)(qint64(1), inner / 2);
|
|
}
|
|
radiusDev = (std::max)(qint64(1), (std::min)(radiusDev, maxRadiusDev));
|
|
const auto radius = fromDev(radiusDev, dpr);
|
|
|
|
if (radius <= 0) {
|
|
return;
|
|
}
|
|
|
|
const auto phase = m_waveProgress * 2 * M_PI;
|
|
const auto arcLen = radius * fullAngleRad;
|
|
const auto len = m_fullLength > 0 ? m_fullLength : arcLen;
|
|
const auto drawAngleRad = fullAngleRad * m_value;
|
|
|
|
if (drawAngleRad <= 0) {
|
|
return;
|
|
}
|
|
|
|
// Sample every device pixel of arc length, not every logical one: at a
|
|
// fractional ratio a logical step is more than one device pixel wide, and
|
|
// the edge would then be interpolated across the pixels it crosses.
|
|
const auto N =
|
|
qMax(64, qCeil(static_cast<double>(toDev(radius, dpr)) * drawAngleRad));
|
|
const auto dTheta = drawAngleRad / N;
|
|
|
|
painter->beginPath();
|
|
|
|
for (int i = 0; i <= N; ++i) {
|
|
const auto theta = startAngleRad + i * dTheta;
|
|
const auto s = i * dTheta * radius;
|
|
const auto phi = m_frequency * 2 * M_PI * (s + m_startX) / len + phase;
|
|
const auto r = radius + amplitude * qSin(phi);
|
|
const auto px = cx + r * qCos(theta);
|
|
const auto py = cy + r * qSin(theta);
|
|
if (i == 0) {
|
|
painter->moveTo(px, py);
|
|
} else {
|
|
painter->lineTo(px, py);
|
|
}
|
|
}
|
|
|
|
painter->stroke();
|
|
}
|
|
|
|
} // namespace ZShell::controls
|