VectorView.cpp 13 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329
  1. /* VectorView.cpp - implementation of VectorView class.
  2. *
  3. * Copyright (c) 2019 Martin Pavelek <he29/dot/HS/at/gmail/dot/com>
  4. *
  5. * This file is part of LMMS - https://lmms.io
  6. * This program is free software; you can redistribute it and/or
  7. * modify it under the terms of the GNU General Public
  8. * License as published by the Free Software Foundation; either
  9. * version 2 of the License, or (at your option) any later version.
  10. *
  11. * This program is distributed in the hope that it will be useful,
  12. * but WITHOUT ANY WARRANTY; without even the implied warranty of
  13. * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
  14. * General Public License for more details.
  15. *
  16. * You should have received a copy of the GNU General Public
  17. * License along with this program (see COPYING); if not, write to the
  18. * Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor,
  19. * Boston, MA 02110-1301 USA.
  20. *
  21. */
  22. #include "VectorView.h"
  23. #include <algorithm>
  24. #include <chrono>
  25. #include <cmath>
  26. #include <QImage>
  27. #include <QPainter>
  28. #include "ColorChooser.h"
  29. #include "GuiApplication.h"
  30. #include "MainWindow.h"
  31. VectorView::VectorView(VecControls *controls, LocklessRingBuffer<sampleFrame> *inputBuffer, unsigned short displaySize, QWidget *parent) :
  32. QWidget(parent),
  33. m_controls(controls),
  34. m_inputBuffer(inputBuffer),
  35. m_bufferReader(*inputBuffer),
  36. m_displaySize(displaySize),
  37. m_zoom(1.f),
  38. m_persistTimestamp(0),
  39. m_zoomTimestamp(0),
  40. m_oldHQ(m_controls->m_highQualityModel.value()),
  41. m_oldX(m_displaySize / 2),
  42. m_oldY(m_displaySize / 2)
  43. {
  44. setMinimumSize(200, 200);
  45. setSizePolicy(QSizePolicy::Expanding, QSizePolicy::Expanding);
  46. connect(gui->mainWindow(), SIGNAL(periodicUpdate()), this, SLOT(periodicUpdate()));
  47. m_displayBuffer.resize(sizeof qRgb(0,0,0) * m_displaySize * m_displaySize, 0);
  48. #ifdef VEC_DEBUG
  49. m_executionAvg = 0;
  50. #endif
  51. }
  52. // Compose and draw all the content; called by Qt.
  53. void VectorView::paintEvent(QPaintEvent *event)
  54. {
  55. #ifdef VEC_DEBUG
  56. unsigned int drawTime = std::chrono::high_resolution_clock::now().time_since_epoch().count();
  57. #endif
  58. // All drawing done in this method, local variables are sufficient for the boundary
  59. const int displayTop = 2;
  60. const int displayBottom = height() - 2;
  61. const int displayLeft = 2;
  62. const int displayRight = width() - 2;
  63. const int displayWidth = displayRight - displayLeft;
  64. const int displayHeight = displayBottom - displayTop;
  65. const float centerX = displayLeft + (displayWidth / 2.f);
  66. const float centerY = displayTop + (displayWidth / 2.f);
  67. const int margin = 4;
  68. const int gridCorner = 30;
  69. // Setup QPainter and font sizes
  70. QPainter painter(this);
  71. painter.setRenderHint(QPainter::Antialiasing, true);
  72. QFont normalFont, boldFont;
  73. boldFont.setPixelSize(26);
  74. boldFont.setBold(true);
  75. const int labelWidth = 26;
  76. const int labelHeight = 26;
  77. bool hq = m_controls->m_highQualityModel.value();
  78. // Clear display buffer if quality setting was changed
  79. if (hq != m_oldHQ)
  80. {
  81. m_oldHQ = hq;
  82. for (std::size_t i = 0; i < m_displayBuffer.size(); i++)
  83. {
  84. m_displayBuffer.data()[i] = 0;
  85. }
  86. }
  87. // Dim stored image based on persistence setting and elapsed time.
  88. // Update period is limited to 50 ms (20 FPS) for non-HQ mode and 10 ms (100 FPS) for HQ mode.
  89. const unsigned int currentTimestamp = std::chrono::duration_cast<std::chrono::milliseconds>
  90. (
  91. std::chrono::high_resolution_clock::now().time_since_epoch()
  92. ).count();
  93. const unsigned int elapsed = currentTimestamp - m_persistTimestamp;
  94. const unsigned int threshold = hq ? 10 : 50;
  95. if (elapsed > threshold)
  96. {
  97. m_persistTimestamp = currentTimestamp;
  98. // Non-HQ mode uses half the resolution → use limited buffer space.
  99. const std::size_t useableBuffer = hq ? m_displayBuffer.size() : m_displayBuffer.size() / 4;
  100. // The knob value is interpreted on log. scale, otherwise the effect would ramp up too slowly.
  101. // Persistence value specifies fraction of light intensity that remains after 10 ms.
  102. // → Compensate it based on elapsed time (exponential decay).
  103. const float persist = log10(1 + 9 * m_controls->m_persistenceModel.value());
  104. const float persistPerFrame = pow(persist, elapsed / 10.f);
  105. // Note that for simplicity and performance reasons, this implementation only dims all stored
  106. // values by a given factor. A true simulation would also do the inverse of desaturation that
  107. // occurs in high-intensity traces in HQ mode.
  108. for (std::size_t i = 0; i < useableBuffer; i++)
  109. {
  110. m_displayBuffer.data()[i] *= persistPerFrame;
  111. }
  112. }
  113. // Get new samples from the lockless input FIFO buffer
  114. auto inBuffer = m_bufferReader.read_max(m_inputBuffer->capacity());
  115. std::size_t frameCount = inBuffer.size();
  116. // Draw new points on top
  117. float left, right;
  118. int x, y;
  119. const bool logScale = m_controls->m_logarithmicModel.value();
  120. const unsigned short activeSize = hq ? m_displaySize : m_displaySize / 2;
  121. // Helper lambda functions for better readability
  122. // Make sure pixel stays within display bounds:
  123. auto saturate = [=](short pixelPos) {return qBound((short)0, pixelPos, (short)(activeSize - 1));};
  124. // Take existing pixel and brigthen it. Very bright light should reduce saturation and become
  125. // white. This effect is easily approximated by capping elementary colors to 255 individually.
  126. auto updatePixel = [&](unsigned short x, unsigned short y, QColor addedColor)
  127. {
  128. QColor currentColor = ((QRgb*)m_displayBuffer.data())[x + y * activeSize];
  129. currentColor.setRed(std::min(currentColor.red() + addedColor.red(), 255));
  130. currentColor.setGreen(std::min(currentColor.green() + addedColor.green(), 255));
  131. currentColor.setBlue(std::min(currentColor.blue() + addedColor.blue(), 255));
  132. ((QRgb*)m_displayBuffer.data())[x + y * activeSize] = currentColor.rgb();
  133. };
  134. if (hq)
  135. {
  136. // High quality mode: check distance between points and draw a line.
  137. // The longer the line is, the dimmer, simulating real electron trace on luminescent screen.
  138. for (std::size_t frame = 0; frame < frameCount; frame++)
  139. {
  140. float inLeft = inBuffer[frame][0] * m_zoom;
  141. float inRight = inBuffer[frame][1] * m_zoom;
  142. // Scale left and right channel from (-1.0, 1.0) to display range
  143. if (logScale)
  144. {
  145. // To better preserve shapes, the log scale is applied to the distance from origin,
  146. // not the individual channels.
  147. const float distance = sqrt(inLeft * inLeft + inRight * inRight);
  148. const float distanceLog = log10(1 + 9 * abs(distance));
  149. const float angleCos = inLeft / distance;
  150. const float angleSin = inRight / distance;
  151. left = distanceLog * angleCos * (activeSize - 1) / 4;
  152. right = distanceLog * angleSin * (activeSize - 1) / 4;
  153. }
  154. else
  155. {
  156. left = inLeft * (activeSize - 1) / 4;
  157. right = inRight * (activeSize - 1) / 4;
  158. }
  159. // Rotate display coordinates 45 degrees, flip Y axis and make sure the result stays within bounds
  160. x = saturate(right - left + activeSize / 2.f);
  161. y = saturate(activeSize - (right + left + activeSize / 2.f));
  162. // Estimate number of points needed to fill space between the old and new pixel. Cap at 100.
  163. unsigned char points = std::min((int)sqrt((m_oldX - x) * (m_oldX - x) + (m_oldY - y) * (m_oldY - y)), 100);
  164. // Large distance = dim trace. The curve for darker() is choosen so that:
  165. // - no movement (0 points) actually _increases_ brightness slightly,
  166. // - one point between samples = returns exactly the specified color,
  167. // - one to 99 points between samples = follows a sharp "1/x" decaying curve,
  168. // - 100 points between samples = returns approximately 5 % brightness.
  169. // Everything else is discarded (by the 100 point cap) because there is not much to see anyway.
  170. QColor addedColor = m_controls->m_colorFG.darker(75 + 20 * points).rgb();
  171. // Draw the new pixel: the beam sweeps across area that may have been excited before
  172. // → add new value to existing pixel state.
  173. updatePixel(x, y, addedColor);
  174. // Draw interpolated points between the old pixel and the new one
  175. int newX = right - left + activeSize / 2.f;
  176. int newY = activeSize - (right + left + activeSize / 2.f);
  177. for (unsigned char i = 1; i < points; i++)
  178. {
  179. x = saturate(((points - i) * m_oldX + i * newX) / points);
  180. y = saturate(((points - i) * m_oldY + i * newY) / points);
  181. updatePixel(x, y, addedColor);
  182. }
  183. m_oldX = newX;
  184. m_oldY = newY;
  185. }
  186. }
  187. else
  188. {
  189. // To improve performance, non-HQ mode uses smaller display size and only
  190. // one full-color pixel per sample.
  191. for (std::size_t frame = 0; frame < frameCount; frame++)
  192. {
  193. float inLeft = inBuffer[frame][0] * m_zoom;
  194. float inRight = inBuffer[frame][1] * m_zoom;
  195. if (logScale) {
  196. const float distance = sqrt(inLeft * inLeft + inRight * inRight);
  197. const float distanceLog = log10(1 + 9 * abs(distance));
  198. const float angleCos = inLeft / distance;
  199. const float angleSin = inRight / distance;
  200. left = distanceLog * angleCos * (activeSize - 1) / 4;
  201. right = distanceLog * angleSin * (activeSize - 1) / 4;
  202. } else {
  203. left = inLeft * (activeSize - 1) / 4;
  204. right = inRight * (activeSize - 1) / 4;
  205. }
  206. x = saturate(right - left + activeSize / 2.f);
  207. y = saturate(activeSize - (right + left + activeSize / 2.f));
  208. ((QRgb*)m_displayBuffer.data())[x + y * activeSize] = m_controls->m_colorFG.rgb();
  209. }
  210. }
  211. // Draw background
  212. painter.fillRect(displayLeft, displayTop, displayWidth, displayHeight, QColor(0,0,0));
  213. // Draw the final image
  214. QImage temp = QImage(m_displayBuffer.data(),
  215. activeSize,
  216. activeSize,
  217. QImage::Format_RGB32);
  218. temp.setDevicePixelRatio(devicePixelRatio());
  219. painter.drawImage(displayLeft, displayTop,
  220. temp.scaledToWidth(displayWidth * devicePixelRatio(),
  221. Qt::SmoothTransformation));
  222. // Draw the grid and labels
  223. painter.setPen(QPen(m_controls->m_colorGrid, 1.5, Qt::SolidLine, Qt::RoundCap, Qt::BevelJoin));
  224. painter.drawEllipse(QPointF(centerX, centerY), displayWidth / 2.f, displayWidth / 2.f);
  225. painter.setPen(QPen(m_controls->m_colorGrid, 1.5, Qt::DotLine, Qt::RoundCap, Qt::BevelJoin));
  226. painter.drawLine(QPointF(centerX, centerY), QPointF(displayLeft + gridCorner, displayTop + gridCorner));
  227. painter.drawLine(QPointF(centerX, centerY), QPointF(displayRight - gridCorner, displayTop + gridCorner));
  228. painter.setPen(QPen(m_controls->m_colorLabels, 1, Qt::SolidLine, Qt::RoundCap, Qt::BevelJoin));
  229. painter.setFont(boldFont);
  230. painter.drawText(displayLeft + margin, displayTop,
  231. labelWidth, labelHeight, Qt::AlignLeft | Qt::AlignTop | Qt::TextDontClip,
  232. QString("L"));
  233. painter.drawText(displayRight - margin - labelWidth, displayTop,
  234. labelWidth, labelHeight, Qt::AlignRight| Qt::AlignTop | Qt::TextDontClip,
  235. QString("R"));
  236. // Draw the outline
  237. painter.setPen(QPen(m_controls->m_colorOutline, 2, Qt::SolidLine, Qt::RoundCap, Qt::BevelJoin));
  238. painter.drawRoundedRect(1, 1, width() - 2, height() - 2, 2.f, 2.f);
  239. // Draw zoom info if changed within last second (re-using timestamp acquired for dimming)
  240. if (currentTimestamp - m_zoomTimestamp < 1000)
  241. {
  242. painter.setPen(QPen(m_controls->m_colorLabels, 1, Qt::SolidLine, Qt::RoundCap, Qt::BevelJoin));
  243. painter.setFont(normalFont);
  244. painter.drawText(displayWidth / 2 - 50, displayBottom - 20, 100, 16, Qt::AlignCenter,
  245. QString("Zoom: ").append(std::to_string((int)round(m_zoom * 100)).c_str()).append(" %"));
  246. }
  247. // Optionally measure drawing performance
  248. #ifdef VEC_DEBUG
  249. drawTime = std::chrono::high_resolution_clock::now().time_since_epoch().count() - drawTime;
  250. m_executionAvg = 0.95f * m_executionAvg + 0.05f * drawTime / 1000000.f;
  251. painter.setPen(QPen(m_controls->m_colorLabels, 1, Qt::SolidLine, Qt::RoundCap, Qt::BevelJoin));
  252. painter.setFont(normalFont);
  253. painter.drawText(displayWidth / 2 - 50, displayBottom - 16, 100, 16, Qt::AlignLeft,
  254. QString("Exec avg.: ").append(std::to_string(m_executionAvg).substr(0, 5).c_str()).append(" ms"));
  255. #endif
  256. }
  257. // Periodically trigger repaint and check if the widget is visible
  258. void VectorView::periodicUpdate()
  259. {
  260. m_visible = isVisible();
  261. if (m_visible) {update();}
  262. }
  263. // Allow to change color on double-click.
  264. // More of an Easter egg, to avoid cluttering the interface with non-essential functionality.
  265. void VectorView::mouseDoubleClickEvent(QMouseEvent *event)
  266. {
  267. ColorChooser *colorDialog = new ColorChooser(m_controls->m_colorFG, this);
  268. if (colorDialog->exec())
  269. {
  270. m_controls->m_colorFG = colorDialog->currentColor();
  271. }
  272. }
  273. // Change zoom level using the mouse wheel
  274. void VectorView::wheelEvent(QWheelEvent *event)
  275. {
  276. // Go through integers to avoid accumulating errors
  277. const unsigned short old_zoom = round(100 * m_zoom);
  278. // Min-max bounds are 20 and 1000 %, step for 15°-increment mouse wheel is 20 %
  279. const unsigned short new_zoom = qBound(20, old_zoom + event->angleDelta().y() / 6, 1000);
  280. m_zoom = new_zoom / 100.f;
  281. event->accept();
  282. m_zoomTimestamp = std::chrono::duration_cast<std::chrono::milliseconds>
  283. (
  284. std::chrono::high_resolution_clock::now().time_since_epoch()
  285. ).count();
  286. }