mirror of
https://github.com/LMMS/lmms.git
synced 2026-03-05 06:36:14 -05:00
329 lines
13 KiB
C++
329 lines
13 KiB
C++
/* 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 * 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::milliseconds>
|
|
(
|
|
std::chrono::high_resolution_clock::now().time_since_epoch()
|
|
).count();
|
|
|
|
}
|