/* VectorView.cpp - implementation of VectorView class. * * Copyright (c) 2019 Martin Pavelek * * This file is part of LMMS - https://lmms.io * This program is free software; you can redistribute it and/or * modify it under the terms of the GNU General Public * License as published by the Free Software Foundation; either * version 2 of the License, or (at your option) any later version. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU * General Public License for more details. * * You should have received a copy of the GNU General Public * License along with this program (see COPYING); if not, write to the * Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, * Boston, MA 02110-1301 USA. * */ #include "VectorView.h" #include #include #include #include #include #include "ColorChooser.h" #include "GuiApplication.h" #include "MainWindow.h" VectorView::VectorView(VecControls *controls, LocklessRingBuffer *inputBuffer, unsigned short displaySize, QWidget *parent) : QWidget(parent), m_controls(controls), m_inputBuffer(inputBuffer), m_bufferReader(*inputBuffer), m_displaySize(displaySize), m_zoom(1.f), m_persistTimestamp(0), m_zoomTimestamp(0), m_oldHQ(m_controls->m_highQualityModel.value()), m_oldX(m_displaySize / 2), m_oldY(m_displaySize / 2) { setMinimumSize(200, 200); setSizePolicy(QSizePolicy::Expanding, QSizePolicy::Expanding); connect(gui->mainWindow(), SIGNAL(periodicUpdate()), this, SLOT(periodicUpdate())); m_displayBuffer.resize(sizeof qRgb(0,0,0) * m_displaySize * m_displaySize, 0); #ifdef VEC_DEBUG m_executionAvg = 0; #endif } // Compose and draw all the content; called by Qt. void VectorView::paintEvent(QPaintEvent *event) { #ifdef VEC_DEBUG unsigned int drawTime = std::chrono::high_resolution_clock::now().time_since_epoch().count(); #endif // All drawing done in this method, local variables are sufficient for the boundary const int displayTop = 2; const int displayBottom = height() - 2; const int displayLeft = 2; const int displayRight = width() - 2; const int displayWidth = displayRight - displayLeft; const int displayHeight = displayBottom - displayTop; const float centerX = displayLeft + (displayWidth / 2.f); const float centerY = displayTop + (displayWidth / 2.f); const int margin = 4; const int gridCorner = 30; // Setup QPainter and font sizes QPainter painter(this); painter.setRenderHint(QPainter::Antialiasing, true); QFont normalFont, boldFont; boldFont.setPixelSize(26); boldFont.setBold(true); const int labelWidth = 26; const int labelHeight = 26; bool hq = m_controls->m_highQualityModel.value(); // Clear display buffer if quality setting was changed if (hq != m_oldHQ) { m_oldHQ = hq; for (std::size_t i = 0; i < m_displayBuffer.size(); i++) { m_displayBuffer.data()[i] = 0; } } // Dim stored image based on persistence setting and elapsed time. // Update period is limited to 50 ms (20 FPS) for non-HQ mode and 10 ms (100 FPS) for HQ mode. const unsigned int currentTimestamp = std::chrono::duration_cast ( std::chrono::high_resolution_clock::now().time_since_epoch() ).count(); const unsigned int elapsed = currentTimestamp - m_persistTimestamp; const unsigned int threshold = hq ? 10 : 50; if (elapsed > threshold) { m_persistTimestamp = currentTimestamp; // Non-HQ mode uses half the resolution → use limited buffer space. const std::size_t useableBuffer = hq ? m_displayBuffer.size() : m_displayBuffer.size() / 4; // The knob value is interpreted on log. scale, otherwise the effect would ramp up too slowly. // Persistence value specifies fraction of light intensity that remains after 10 ms. // → Compensate it based on elapsed time (exponential decay). const float persist = log10(1 + 9 * m_controls->m_persistenceModel.value()); const float persistPerFrame = pow(persist, elapsed / 10.f); // Note that for simplicity and performance reasons, this implementation only dims all stored // values by a given factor. A true simulation would also do the inverse of desaturation that // occurs in high-intensity traces in HQ mode. for (std::size_t i = 0; i < useableBuffer; i++) { m_displayBuffer.data()[i] *= persistPerFrame; } } // Get new samples from the lockless input FIFO buffer auto inBuffer = m_bufferReader.read_max(m_inputBuffer->capacity()); std::size_t frameCount = inBuffer.size(); // Draw new points on top float left, right; int x, y; const bool logScale = m_controls->m_logarithmicModel.value(); const unsigned short activeSize = hq ? m_displaySize : m_displaySize / 2; // Helper lambda functions for better readability // Make sure pixel stays within display bounds: auto saturate = [=](short pixelPos) {return qBound((short)0, pixelPos, (short)(activeSize - 1));}; // Take existing pixel and brigthen it. Very bright light should reduce saturation and become // white. This effect is easily approximated by capping elementary colors to 255 individually. auto updatePixel = [&](unsigned short x, unsigned short y, QColor addedColor) { QColor currentColor = ((QRgb*)m_displayBuffer.data())[x + y * activeSize]; currentColor.setRed(std::min(currentColor.red() + addedColor.red(), 255)); currentColor.setGreen(std::min(currentColor.green() + addedColor.green(), 255)); currentColor.setBlue(std::min(currentColor.blue() + addedColor.blue(), 255)); ((QRgb*)m_displayBuffer.data())[x + y * activeSize] = currentColor.rgb(); }; if (hq) { // High quality mode: check distance between points and draw a line. // The longer the line is, the dimmer, simulating real electron trace on luminescent screen. for (std::size_t frame = 0; frame < frameCount; frame++) { float inLeft = inBuffer[frame][0] * m_zoom; float inRight = inBuffer[frame][1] * m_zoom; // Scale left and right channel from (-1.0, 1.0) to display range if (logScale) { // To better preserve shapes, the log scale is applied to the distance from origin, // not the individual channels. const float distance = sqrt(inLeft * inLeft + inRight * inRight); const float distanceLog = log10(1 + 9 * abs(distance)); const float angleCos = inLeft / distance; const float angleSin = inRight / distance; left = distanceLog * angleCos * (activeSize - 1) / 4; right = distanceLog * angleSin * (activeSize - 1) / 4; } else { left = inLeft * (activeSize - 1) / 4; right = inRight * (activeSize - 1) / 4; } // Rotate display coordinates 45 degrees, flip Y axis and make sure the result stays within bounds x = saturate(right - left + activeSize / 2.f); y = saturate(activeSize - (right + left + activeSize / 2.f)); // Estimate number of points needed to fill space between the old and new pixel. Cap at 100. unsigned char points = std::min((int)sqrt((m_oldX - x) * (m_oldX - x) + (m_oldY - y) * (m_oldY - y)), 100); // Large distance = dim trace. The curve for darker() is choosen so that: // - no movement (0 points) actually _increases_ brightness slightly, // - one point between samples = returns exactly the specified color, // - one to 99 points between samples = follows a sharp "1/x" decaying curve, // - 100 points between samples = returns approximately 5 % brightness. // Everything else is discarded (by the 100 point cap) because there is not much to see anyway. QColor addedColor = m_controls->m_colorFG.darker(75 + 20 * points).rgb(); // Draw the new pixel: the beam sweeps across area that may have been excited before // → add new value to existing pixel state. updatePixel(x, y, addedColor); // Draw interpolated points between the old pixel and the new one int newX = right - left + activeSize / 2.f; int newY = activeSize - (right + left + activeSize / 2.f); for (unsigned char i = 1; i < points; i++) { x = saturate(((points - i) * m_oldX + i * newX) / points); y = saturate(((points - i) * m_oldY + i * newY) / points); updatePixel(x, y, addedColor); } m_oldX = newX; m_oldY = newY; } } else { // To improve performance, non-HQ mode uses smaller display size and only // one full-color pixel per sample. for (std::size_t frame = 0; frame < frameCount; frame++) { float inLeft = inBuffer[frame][0] * m_zoom; float inRight = inBuffer[frame][1] * m_zoom; if (logScale) { const float distance = sqrt(inLeft * inLeft + inRight * inRight); const float distanceLog = log10(1 + 9 * abs(distance)); const float angleCos = inLeft / distance; const float angleSin = inRight / distance; left = distanceLog * angleCos * (activeSize - 1) / 4; right = distanceLog * angleSin * (activeSize - 1) / 4; } else { left = inLeft * (activeSize - 1) / 4; right = inRight * (activeSize - 1) / 4; } x = saturate(right - left + activeSize / 2.f); y = saturate(activeSize - (right + left + activeSize / 2.f)); ((QRgb*)m_displayBuffer.data())[x + y * activeSize] = m_controls->m_colorFG.rgb(); } } // Draw background painter.fillRect(displayLeft, displayTop, displayWidth, displayHeight, QColor(0,0,0)); // Draw the final image QImage temp = QImage(m_displayBuffer.data(), activeSize, activeSize, QImage::Format_RGB32); temp.setDevicePixelRatio(devicePixelRatio()); painter.drawImage(displayLeft, displayTop, temp.scaledToWidth(displayWidth * devicePixelRatio(), Qt::SmoothTransformation)); // Draw the grid and labels painter.setPen(QPen(m_controls->m_colorGrid, 1.5, Qt::SolidLine, Qt::RoundCap, Qt::BevelJoin)); painter.drawEllipse(QPointF(centerX, centerY), displayWidth / 2.f, displayWidth / 2.f); painter.setPen(QPen(m_controls->m_colorGrid, 1.5, Qt::DotLine, Qt::RoundCap, Qt::BevelJoin)); painter.drawLine(QPointF(centerX, centerY), QPointF(displayLeft + gridCorner, displayTop + gridCorner)); painter.drawLine(QPointF(centerX, centerY), QPointF(displayRight - gridCorner, displayTop + gridCorner)); painter.setPen(QPen(m_controls->m_colorLabels, 1, Qt::SolidLine, Qt::RoundCap, Qt::BevelJoin)); painter.setFont(boldFont); painter.drawText(displayLeft + margin, displayTop, labelWidth, labelHeight, Qt::AlignLeft | Qt::AlignTop | Qt::TextDontClip, QString("L")); painter.drawText(displayRight - margin - labelWidth, displayTop, labelWidth, labelHeight, Qt::AlignRight| Qt::AlignTop | Qt::TextDontClip, QString("R")); // Draw the outline painter.setPen(QPen(m_controls->m_colorOutline, 2, Qt::SolidLine, Qt::RoundCap, Qt::BevelJoin)); painter.drawRoundedRect(1, 1, width() - 2, height() - 2, 2.f, 2.f); // Draw zoom info if changed within last second (re-using timestamp acquired for dimming) if (currentTimestamp - m_zoomTimestamp < 1000) { painter.setPen(QPen(m_controls->m_colorLabels, 1, Qt::SolidLine, Qt::RoundCap, Qt::BevelJoin)); painter.setFont(normalFont); painter.drawText(displayWidth / 2 - 50, displayBottom - 20, 100, 16, Qt::AlignCenter, QString("Zoom: ").append(std::to_string((int)round(m_zoom * 100)).c_str()).append(" %")); } // Optionally measure drawing performance #ifdef VEC_DEBUG drawTime = std::chrono::high_resolution_clock::now().time_since_epoch().count() - drawTime; m_executionAvg = 0.95f * m_executionAvg + 0.05f * drawTime / 1000000.f; painter.setPen(QPen(m_controls->m_colorLabels, 1, Qt::SolidLine, Qt::RoundCap, Qt::BevelJoin)); painter.setFont(normalFont); painter.drawText(displayWidth / 2 - 50, displayBottom - 16, 100, 16, Qt::AlignLeft, QString("Exec avg.: ").append(std::to_string(m_executionAvg).substr(0, 5).c_str()).append(" ms")); #endif } // Periodically trigger repaint and check if the widget is visible void VectorView::periodicUpdate() { m_visible = isVisible(); if (m_visible) {update();} } // Allow to change color on double-click. // More of an Easter egg, to avoid cluttering the interface with non-essential functionality. void VectorView::mouseDoubleClickEvent(QMouseEvent *event) { ColorChooser *colorDialog = new ColorChooser(m_controls->m_colorFG, this); if (colorDialog->exec()) { m_controls->m_colorFG = colorDialog->currentColor(); } } // Change zoom level using the mouse wheel void VectorView::wheelEvent(QWheelEvent *event) { // Go through integers to avoid accumulating errors const unsigned short old_zoom = round(100 * m_zoom); // Min-max bounds are 20 and 1000 %, step for 15°-increment mouse wheel is 20 % const unsigned short new_zoom = qBound(20, old_zoom + event->angleDelta().y() / 6, 1000); m_zoom = new_zoom / 100.f; event->accept(); m_zoomTimestamp = std::chrono::duration_cast ( std::chrono::high_resolution_clock::now().time_since_epoch() ).count(); }