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()
################################################################