123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329 |
- /* VectorView.cpp - implementation of VectorView class.
- *
- * Copyright (c) 2019 Martin Pavelek <he29/dot/HS/at/gmail/dot/com>
- *
- * 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 <algorithm>
- #include <chrono>
- #include <cmath>
- #include <QImage>
- #include <QPainter>
- #include "ColorChooser.h"
- #include "GuiApplication.h"
- #include "MainWindow.h"
- VectorView::VectorView(VecControls *controls, LocklessRingBuffer<sampleFrame> *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::milliseconds>
- (
- 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 * std::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 * std::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::milliseconds>
- (
- std::chrono::high_resolution_clock::now().time_since_epoch()
- ).count();
- }
|