diff --git a/src/util/roundtopixel.h b/src/util/roundtopixel.h new file mode 100644 index 00000000000..7ab5c3f68c9 --- /dev/null +++ b/src/util/roundtopixel.h @@ -0,0 +1,9 @@ +#pragma once + +#include + +inline auto createFunctionRoundToPixel(float devicePixelRatio) { + return [devicePixelRatio](float pos) { + return std::round(pos * devicePixelRatio) / devicePixelRatio; + }; +} diff --git a/src/waveform/renderers/allshader/digitsrenderer.cpp b/src/waveform/renderers/allshader/digitsrenderer.cpp index e6246850d3e..cd7d9947e5d 100644 --- a/src/waveform/renderers/allshader/digitsrenderer.cpp +++ b/src/waveform/renderers/allshader/digitsrenderer.cpp @@ -10,7 +10,8 @@ #include #include -#include "./util/assert.h" +#include "util/assert.h" +#include "util/roundtopixel.h" #include "waveform/renderers/allshader/matrixforwidgetgeometry.h" #include "waveform/renderers/allshader/vertexdata.h" @@ -85,13 +86,12 @@ void allshader::DigitsRenderer::updateTexture( } } - qreal space; + float space; QFont font; QFontMetricsF metrics{font}; font.setFamily("Open Sans"); - qreal totalTextWidth; - qreal maxTextHeight; + float maxTextHeight; bool retry = false; do { // At small sizes, we need to limit the pen width, to avoid drawing artifacts. @@ -100,21 +100,19 @@ void allshader::DigitsRenderer::updateTexture( // The pen width is twice the outline size m_penWidth = std::min(maxPenWidth, OUTLINE_SIZE * 2); - space = static_cast(m_penWidth) / 2; + space = static_cast(m_penWidth) / 2; font.setPointSizeF(fontPointSize); - const qreal maxHeightWithoutSpace = std::floor(maxHeight) - space * 2 - 1; + const float maxHeightWithoutSpace = std::floor(maxHeight) - space * 2 - 1; metrics = QFontMetricsF{font}; - totalTextWidth = 0; maxTextHeight = 0; for (int i = 0; i < NUM_CHARS; i++) { const QString text(indexToChar(i)); const auto rect = metrics.tightBoundingRect(text); - maxTextHeight = std::max(maxTextHeight, rect.height()); - totalTextWidth += metrics.horizontalAdvance(text); + maxTextHeight = std::max(maxTextHeight, static_cast(rect.height())); } if (m_adjustedFontPointSize == 0.f && !retry && maxTextHeight > maxHeightWithoutSpace) { // We need to adjust the font size to fit in the maxHeight. @@ -129,13 +127,28 @@ void allshader::DigitsRenderer::updateTexture( } } while (retry); - m_height = static_cast(std::ceil(maxTextHeight) + space * 2 + 1); + m_height = static_cast(std::ceil(maxTextHeight)) + space * 2.f + 1.f; - // Space around the digits - totalTextWidth += (space * 2 + 1) * NUM_CHARS; - totalTextWidth = std::ceil(totalTextWidth); + const float y = maxTextHeight + space - 0.5f; - const qreal y = maxTextHeight + space - 0.5; + auto roundToPixel = createFunctionRoundToPixel(devicePixelRatio); + + float totalTextWidth{}; + std::array xs; + // determine x position and with of each of the chars in the texture image. + for (int i = 0; i < NUM_CHARS; i++) { + xs[i] = totalTextWidth; + float w = roundToPixel(static_cast( + metrics.horizontalAdvance(indexToChar(i)))) + + space + space + 1.f; + totalTextWidth += w; + m_width[i] = static_cast(w); + } + for (int i = 0; i < NUM_CHARS; i++) { + // position of character at index i in the texture, normalized + m_offset[i] = static_cast(xs[i] / totalTextWidth); + } + m_offset[NUM_CHARS] = 1.f; QImage image(std::lround(totalTextWidth * devicePixelRatio), std::lround(m_height * devicePixelRatio), @@ -153,12 +166,10 @@ void allshader::DigitsRenderer::updateTexture( painter.setBrush(QColor(0, 0, 0, OUTLINE_ALPHA)); painter.setPen(pen); painter.setFont(font); - qreal x = 0; QPainterPath path; for (int i = 0; i < NUM_CHARS; i++) { const QString text(indexToChar(i)); - path.addText(QPointF(x + space + 0.5, y), font, text); - x += metrics.horizontalAdvance(text) + space + space + 1; + path.addText(QPointF(xs[i] + space + 0.5, y), font, text); } painter.drawPath(path); } @@ -166,7 +177,7 @@ void allshader::DigitsRenderer::updateTexture( { // Apply Gaussian blur to dark outline auto blur = std::make_unique(); - blur->setBlurRadius(static_cast(m_penWidth) / 3); + blur->setBlurRadius(static_cast(m_penWidth) / 3); QGraphicsScene scene; auto item = std::make_unique(); @@ -187,19 +198,12 @@ void allshader::DigitsRenderer::updateTexture( painter.setPen(Qt::white); painter.setBrush(Qt::white); - qreal x = 0; QPainterPath path; for (int i = 0; i < NUM_CHARS; i++) { const QString text(indexToChar(i)); - path.addText(QPointF(x + space + 0.5, y), font, text); - // position and width of character at index i in the texture - m_offset[i] = static_cast(x / totalTextWidth); - const auto xp = x; - x += metrics.horizontalAdvance(text) + space + space + 1; - m_width[i] = static_cast(x - xp); + path.addText(QPointF(xs[i] + space + 0.5, y), font, text); } painter.drawPath(path); - m_offset[NUM_CHARS] = 1.f; } m_texture.setData(image); diff --git a/src/waveform/renderers/allshader/waveformrendermark.cpp b/src/waveform/renderers/allshader/waveformrendermark.cpp index 13aff227095..680eedcb650 100644 --- a/src/waveform/renderers/allshader/waveformrendermark.cpp +++ b/src/waveform/renderers/allshader/waveformrendermark.cpp @@ -5,6 +5,7 @@ #include "track/track.h" #include "util/colorcomponents.h" +#include "util/roundtopixel.h" #include "waveform/renderers/allshader/matrixforwidgetgeometry.h" #include "waveform/renderers/allshader/rgbadata.h" #include "waveform/renderers/allshader/vertexdata.h" @@ -20,6 +21,8 @@ // only to draw on a QImage. This is only done once when needed and the images are // then used as textures to be drawn with a GLSL shader. +namespace { + class TextureGraphics : public WaveformMark::Graphics { public: TextureGraphics(const QImage& image) { @@ -33,19 +36,9 @@ class TextureGraphics : public WaveformMark::Graphics { OpenGLTexture2D m_texture; }; -// Both allshader::WaveformRenderMark and the non-GL ::WaveformRenderMark derive -// from WaveformRenderMarkBase. The base-class takes care of updating the marks -// when needed and flagging them when their image needs to be updated (resizing, -// cue changes, position changes) -// -// While in the case of ::WaveformRenderMark those images can be updated immediately, -// in the case of allshader::WaveformRenderMark we need to do that when we have an -// openGL context, as we create new textures. -// -// The boolean argument for the WaveformRenderMarkBase constructor indicates -// that updateMarkImages should not be called immediately. +constexpr float kPlayPosWidth{11.f}; +constexpr float kPlayPosOffset{-(kPlayPosWidth - 1.f) / 2.f}; -namespace { QString timeSecToString(double timeSec) { int hundredths = std::lround(timeSec * 100.0); int seconds = hundredths / 100; @@ -58,6 +51,18 @@ QString timeSecToString(double timeSec) { } // namespace +// Both allshader::WaveformRenderMark and the non-GL ::WaveformRenderMark derive +// from WaveformRenderMarkBase. The base-class takes care of updating the marks +// when needed and flagging them when their image needs to be updated (resizing, +// cue changes, position changes) +// +// While in the case of ::WaveformRenderMark those images can be updated immediately, +// in the case of allshader::WaveformRenderMark we need to do that when we have an +// openGL context, as we create new textures. +// +// The boolean argument for the WaveformRenderMarkBase constructor indicates +// that updateMarkImages should not be called immediately. + allshader::WaveformRenderMark::WaveformRenderMark( WaveformWidgetRenderer* waveformWidget, ::WaveformRendererAbstract::PositionSource type) @@ -204,6 +209,8 @@ void allshader::WaveformRenderMark::paintGL() { glEnable(GL_BLEND); glBlendFunc(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA); + auto roundToPixel = createFunctionRoundToPixel(devicePixelRatio); + for (const auto& pMark : std::as_const(m_marks)) { pMark->setBreadth(slipActive ? m_waveformRenderer->getBreadth() / 2 : m_waveformRenderer->getBreadth()); @@ -235,14 +242,9 @@ void allshader::WaveformRenderMark::paintGL() { continue; } - const float currentMarkPoint = - std::round( - static_cast( - m_waveformRenderer - ->transformSamplePositionInRendererWorld( - samplePosition, positionType)) * - devicePixelRatio) / - devicePixelRatio; + const float currentMarkPos = static_cast( + m_waveformRenderer->transformSamplePositionInRendererWorld( + samplePosition, positionType)); if (pMark->isShowUntilNext() && samplePosition >= playPosition + 1.0 && samplePosition < nextMarkPosition) { @@ -250,21 +252,17 @@ void allshader::WaveformRenderMark::paintGL() { } const double sampleEndPosition = pMark->getSampleEndPosition(); - // Pixmaps are expected to have the mark stroke at the center, - // and preferably have an odd width in order to have the stroke - // exactly at the sample position. - const float markHalfWidth = pTexture->width() / devicePixelRatio / 2.f; - const float drawOffset = currentMarkPoint - markHalfWidth; + const float markWidth = pTexture->width() / devicePixelRatio; + const float drawOffset = currentMarkPos + pMark->getOffset(); bool visible = false; // Check if the current point needs to be displayed. - if (drawOffset > -markHalfWidth && - drawOffset < m_waveformRenderer->getLength() + - markHalfWidth) { + if (drawOffset > -markWidth && + drawOffset < m_waveformRenderer->getLength()) { drawTexture(matrix, - drawOffset, + roundToPixel(drawOffset), !m_isSlipRenderer && slipActive - ? m_waveformRenderer->getBreadth() / 2 + ? roundToPixel(m_waveformRenderer->getBreadth() / 2.f) : 0, pTexture); visible = true; @@ -273,19 +271,16 @@ void allshader::WaveformRenderMark::paintGL() { // Check if the range needs to be displayed. if (samplePosition != sampleEndPosition && sampleEndPosition != Cue::kNoPosition) { DEBUG_ASSERT(samplePosition < sampleEndPosition); - const float currentMarkEndPoint = static_cast< - float>( - m_waveformRenderer - ->transformSamplePositionInRendererWorld( - sampleEndPosition, positionType)); - - if (visible || currentMarkEndPoint > 0) { + const float currentMarkEndPos = static_cast( + m_waveformRenderer->transformSamplePositionInRendererWorld( + sampleEndPosition, positionType)); + if (visible || currentMarkEndPos > 0.f) { QColor color = pMark->fillColor(); color.setAlphaF(0.4f); drawMark(matrix, - QRectF(QPointF(currentMarkPoint, 0), - QPointF(currentMarkEndPoint, + QRectF(QPointF(roundToPixel(currentMarkPos), 0), + QPointF(roundToPixel(currentMarkEndPos), m_waveformRenderer ->getBreadth())), color); @@ -301,16 +296,10 @@ void allshader::WaveformRenderMark::paintGL() { } m_waveformRenderer->setMarkPositions(marksOnScreen); - const float currentMarkPoint = - std::round(static_cast( - m_waveformRenderer->getPlayMarkerPosition() * - m_waveformRenderer->getLength()) * - devicePixelRatio) / - devicePixelRatio; - + const float playMarkerPos = static_cast(m_waveformRenderer->getPlayMarkerPosition() * + m_waveformRenderer->getLength()); if (m_playPosMarkTexture.isStorageAllocated()) { - const float markHalfWidth = m_playPosMarkTexture.width() / devicePixelRatio / 2.f; - const float drawOffset = currentMarkPoint - markHalfWidth; + const float drawOffset = roundToPixel(playMarkerPos + kPlayPosOffset); drawTexture(matrix, drawOffset, 0.f, &m_playPosMarkTexture); } @@ -318,7 +307,7 @@ void allshader::WaveformRenderMark::paintGL() { if (WaveformWidgetFactory::instance()->getUntilMarkShowBeats() || WaveformWidgetFactory::instance()->getUntilMarkShowTime()) { updateUntilMark(playPosition, nextMarkPosition); - drawUntilMark(matrix, currentMarkPoint + 20); + drawUntilMark(matrix, roundToPixel(playMarkerPos + 20.f)); } } @@ -395,7 +384,7 @@ void allshader::WaveformRenderMark::updatePlayPosMarkTexture() { const float lineX = 5.5f; - imgwidth = 11.f; + imgwidth = kPlayPosWidth; imgheight = height; QImage image(static_cast(imgwidth * devicePixelRatio), diff --git a/src/waveform/renderers/waveformmark.cpp b/src/waveform/renderers/waveformmark.cpp index 809e633e551..e0923845233 100644 --- a/src/waveform/renderers/waveformmark.cpp +++ b/src/waveform/renderers/waveformmark.cpp @@ -100,6 +100,7 @@ WaveformMark::WaveformMark(const QString& group, const WaveformSignalColors& signalColors, int hotCue) : m_linePosition{}, + m_offset{}, m_breadth{}, m_level{}, m_iPriority(priority), @@ -278,6 +279,15 @@ struct MarkerGeometry { const Qt::Alignment alignH = align & Qt::AlignHorizontal_Mask; const Qt::Alignment alignV = align & Qt::AlignVertical_Mask; const bool alignHCenter{alignH == Qt::AlignHCenter}; + + // The image width is the label rect width + 1, so that the label rect + // left and right positions can be at an integer + 0.5. This is so that + // the label rect is drawn at an exact pixel positions. + // + // Likewise, the line position also has to fall on an integer + 0.5. + // When center aligning, the image width has to be odd, so that the + // center is an integer + 0.5. For the image width to be odd, to + // label rect width has to be even. const qreal widthRounding{alignHCenter ? 2.f : 1.f}; m_labelRect = QRectF{0.f, @@ -286,17 +296,9 @@ struct MarkerGeometry { widthRounding, std::ceil(capHeight + 2.f * margin)}; - m_imageSize = QSizeF{alignHCenter ? m_labelRect.width() + 1.f - : 2.f * m_labelRect.width() + 1.f, - breadth}; + m_imageSize = QSizeF{m_labelRect.width() + 1.f, breadth}; - if (alignH == Qt::AlignHCenter) { - m_labelRect.moveLeft((m_imageSize.width() - m_labelRect.width()) / 2.f); - } else if (alignH == Qt::AlignRight) { - m_labelRect.moveRight(m_imageSize.width() - 0.5f); - } else { - m_labelRect.moveLeft(0.5f); - } + m_labelRect.moveLeft(0.5f); const float increment = overlappingMarkerIncrement( static_cast(m_labelRect.height()), breadth); @@ -373,22 +375,41 @@ QImage WaveformMark::generateImage(float devicePixelRatio) { painter.setWorldMatrixEnabled(false); - // Draw marker lines - const auto hcenter = markerGeometry.m_imageSize.width() / 2.f; - m_linePosition = static_cast(hcenter); + const Qt::Alignment alignH = m_align & Qt::AlignHorizontal_Mask; + const float imgw = static_cast(markerGeometry.m_imageSize.width()); + switch (alignH) { + case Qt::AlignHCenter: + m_linePosition = imgw / 2.f; + m_offset = -(imgw - 1.f) / 2.f; + break; + case Qt::AlignLeft: + m_linePosition = imgw - 1.5f; + m_offset = -imgw + 2.f; + break; + case Qt::AlignRight: + default: + m_linePosition = 1.5f; + m_offset = -1.f; + break; + } + + // Note: linePos has to be at integer + 0.5 to draw correctly + const float linePos = m_linePosition; + [[maybe_unused]] const float epsilon = 1e-6f; + DEBUG_ASSERT(std::abs(linePos - std::floor(linePos) - 0.5) < epsilon); // Draw the center line painter.setPen(fillColor()); - painter.drawLine(QLineF(hcenter, 0.f, hcenter, markerGeometry.m_imageSize.height())); + painter.drawLine(QLineF(linePos, 0.f, linePos, markerGeometry.m_imageSize.height())); painter.setPen(borderColor()); - painter.drawLine(QLineF(hcenter - 1.f, + painter.drawLine(QLineF(linePos - 1.f, 0.f, - hcenter - 1.f, + linePos - 1.f, markerGeometry.m_imageSize.height())); - painter.drawLine(QLineF(hcenter + 1.f, + painter.drawLine(QLineF(linePos + 1.f, 0.f, - hcenter + 1.f, + linePos + 1.f, markerGeometry.m_imageSize.height())); if (useIcon || label.length() != 0) { diff --git a/src/waveform/renderers/waveformmark.h b/src/waveform/renderers/waveformmark.h index 66a9bbc40cf..d01d73b795f 100644 --- a/src/waveform/renderers/waveformmark.h +++ b/src/waveform/renderers/waveformmark.h @@ -37,6 +37,10 @@ class WaveformMark { WaveformMark(const WaveformMark&) = delete; WaveformMark& operator=(const WaveformMark&) = delete; + float getOffset() const { + return m_offset; + } + int getHotCue() const { return m_iHotCue; }; @@ -158,6 +162,7 @@ class WaveformMark { QString m_iconPath; float m_linePosition; + float m_offset; float m_breadth; // When there are overlapping marks, level is increased for each overlapping mark, diff --git a/src/waveform/renderers/waveformrendermark.cpp b/src/waveform/renderers/waveformrendermark.cpp index 4f0c4693f06..fb8688b9138 100644 --- a/src/waveform/renderers/waveformrendermark.cpp +++ b/src/waveform/renderers/waveformrendermark.cpp @@ -37,17 +37,13 @@ void WaveformRenderMark::draw(QPainter* painter, QPaintEvent* /*event*/) { m_waveformRenderer->transformSamplePositionInRendererWorld(samplePosition); const double sampleEndPosition = pMark->getSampleEndPosition(); if (m_waveformRenderer->getOrientation() == Qt::Horizontal) { - // Pixmaps are expected to have the mark stroke at the center, - // and preferably have an odd width in order to have the stroke - // exactly at the sample position. - const int markHalfWidth = - static_cast(image.width() / 2.0 / - m_waveformRenderer->getDevicePixelRatio()); - const int drawOffset = static_cast(currentMarkPoint) - markHalfWidth; + const int markWidth = std::lround(image.width() / + m_waveformRenderer->getDevicePixelRatio()); + const int drawOffset = std::lround(currentMarkPoint + pMark->getOffset()); bool visible = false; // Check if the current point needs to be displayed. - if (currentMarkPoint > -markHalfWidth && currentMarkPoint < m_waveformRenderer->getWidth() + markHalfWidth) { + if (drawOffset > -markWidth && drawOffset < m_waveformRenderer->getWidth()) { painter->drawImage(drawOffset, 0, image); visible = true; } @@ -84,16 +80,15 @@ void WaveformRenderMark::draw(QPainter* painter, QPaintEvent* /*event*/) { pMark, drawOffset}); } } else { - const int markHalfHeight = - static_cast(image.height() / 2.0 / - m_waveformRenderer->getDevicePixelRatio()); - const int drawOffset = static_cast(currentMarkPoint) - markHalfHeight; + const int markHeight = std::lroundf(image.height() / + m_waveformRenderer->getDevicePixelRatio()); + const int drawOffset = + std::lround(static_cast(currentMarkPoint) + + pMark->getOffset()); bool visible = false; // Check if the current point needs to be displayed. - if (currentMarkPoint > -markHalfHeight && - currentMarkPoint < m_waveformRenderer->getHeight() + - markHalfHeight) { + if (drawOffset > -markHeight && drawOffset < m_waveformRenderer->getHeight()) { painter->drawImage(0, drawOffset, image); visible = true; } diff --git a/src/waveform/renderers/waveformwidgetrenderer.cpp b/src/waveform/renderers/waveformwidgetrenderer.cpp index 91e7717649f..8059711b8a2 100644 --- a/src/waveform/renderers/waveformwidgetrenderer.cpp +++ b/src/waveform/renderers/waveformwidgetrenderer.cpp @@ -265,8 +265,8 @@ void WaveformWidgetRenderer::draw(QPainter* painter, QPaintEvent* event) { } void WaveformWidgetRenderer::drawPlayPosmarker(QPainter* painter) { - const int lineX = static_cast(m_width * m_playMarkerPosition); - const int lineY = static_cast(m_height * m_playMarkerPosition); + const int lineX = std::lround(m_width * m_playMarkerPosition); + const int lineY = std::lround(m_height * m_playMarkerPosition); // draw dim outlines to increase playpos/waveform contrast painter->setOpacity(0.5);