#include "wavylinerenderer.hpp" #include "wavyline.hpp" #include #include #include namespace ZShell::controls { void WavyLineRenderer::synchronizeData(QCanvasPainterItem* item) { auto* wavyLine = static_cast(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(sizeDev) / 2.0 - halfParity) + halfParity; } qint64 snapAmplitude(qint64 widthDev, qreal multiplier) { return std::llround(static_cast(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( 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( 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(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