Source code for kf.QtConcentricLoops

"""PyQt5 widgets to render 'MIDI tape loop' generator graphics.
"""

# Inspired by Brian Eno's "Music for Airports" and
# "JavaScript Systems Music" https://teropa.info/blog/2016/07/28/javascript-systems-music.html#brian-enoambient-1-music-for-airports-2-11978

################################################################
# Written in 2019 by Garth Zeglin <garthz@cmu.edu>

# To the extent possible under law, the author has dedicated all copyright
# and related and neighboring rights to this software to the public domain
# worldwide. This software is distributed without any warranty.

# You should have received a copy of the CC0 Public Domain Dedication along with this software.
# If not, see <http://creativecommons.org/publicdomain/zero/1.0/>.

################################################################
import math, logging

# for documentation on the PyQt5 API, see http://pyqt.sourceforge.net/Docs/PyQt5/index.html
from PyQt5 import QtCore, QtGui, QtWidgets

# set up logger for module
log = logging.getLogger('QtConcentricLoops')

# filter out most logging; the default is NOTSET which passes along everything
# log.setLevel(logging.WARNING)

################################################################
[docs]class QtConcentricLoops(QtWidgets.QWidget): """Custom widget representing a MIDI 'tape loop' generator.""" def __init__(self, channels=7): super().__init__() self.setMinimumSize(QtCore.QSize(100, 100)) self.setAutoFillBackground(True) # graphical state variables self.channels = channels self.positions = [0.0]*self.channels # units are degrees # Create a path representing the basic concentric circles structure. # The default coordinates have +X to the right, +Y down. The drawing # units are notionally millimeters (although the graphic is abstract). self.track_symbol = QtGui.QPainterPath() for i in range(channels): r = 25 + 12*i self.track_symbol.addEllipse(QtCore.QPointF(0.0, 0.0), r-5, r-5) # center, rx, ry self.track_symbol.addEllipse(QtCore.QPointF(0.0, 0.0), r+5, r+5) # center, rx, ry self.track_symbol.moveTo(12,0) # draw the play bar self.track_symbol.lineTo(26 + 12*self.channels, 0) # finish initialization self.show() return
[docs] def update_positions(self, positions): N = len(positions) self.positions[0:N] = positions self.repaint()
# === element drawing methods ============================================================ # === Qt API methods ============================================================
[docs] def paintEvent(self, e): """Subclass implementation of parent QWidget class callback to repaint the graphics.""" geometry = self.geometry() view_width = geometry.width() view_height = geometry.height() # clear the background qp = QtGui.QPainter() qp.begin(self) qp.fillRect(QtCore.QRectF(0, 0, view_width, view_height), QtCore.Qt.white) # qp.setRenderHint(QtGui.QPainter.Antialiasing) # Set up a coordinate system scaled to real-world millimeters, centered # in the visible area, keeping the minimum visible area in view. # define minimum visible area scene_width = 20 + 10 + 20 + 24*self.channels scene_height = scene_width scene_aspect = scene_width / scene_height view_aspect = view_width / view_height if scene_aspect > view_aspect: scaling = view_width / scene_width else: scaling = view_height/scene_height qp.save() qp.translate(QtCore.QPointF(view_width/2, view_height/2)) qp.scale(scaling, scaling) # draw the track at scale pen = QtGui.QPen(QtCore.Qt.black) pen.setWidthF(0.5) qp.setPen(pen) brush = QtGui.QBrush(QtGui.QColor(240, 240, 240, 255)) qp.setBrush(brush) qp.drawPath(self.track_symbol) # draw the position markers brush = QtGui.QBrush(QtCore.Qt.red) qp.setBrush(brush) for i, angle in enumerate(self.positions): r = 25 + 12*i theta = angle * math.pi/180 # draw the markers at a position such that angles increase CCW, i.e. +Z is out of the screen qp.drawEllipse(QtCore.QPointF(r * math.cos(theta), -r * math.sin(theta)), 5, 5) # restore the initial unscaled coordinates qp.restore() qp.end()
################################################################