aboutsummaryrefslogtreecommitdiff
path: root/src/avp/components
diff options
context:
space:
mode:
authorBrianna Rainey2026-01-13 19:34:55 -0500
committerGitHub2026-01-13 19:34:55 -0500
commit50f5a76603a3f97f2c6f6a1d3cefea88ed3497aa (patch)
tree226fe223b31af6f217b1dd413629ab2cf26964d4 /src/avp/components
parentb8703752ffc7768b0275897b3c2a869ff41504e5 (diff)
parentf975144f25d34f97329b2d4e52891061573cea08 (diff)
Merge pull request #85 from aeliton/add-pyproject
Use pyproject.toml + uv_build
Diffstat (limited to 'src/avp/components')
-rw-r--r--src/avp/components/__init__.py1
-rw-r--r--src/avp/components/__template__.ui119
-rw-r--r--src/avp/components/color.py176
-rw-r--r--src/avp/components/color.ui666
-rw-r--r--src/avp/components/image.py129
-rw-r--r--src/avp/components/image.ui388
-rw-r--r--src/avp/components/life.py520
-rw-r--r--src/avp/components/life.ui405
-rw-r--r--src/avp/components/original.py243
-rw-r--r--src/avp/components/original.ui243
-rw-r--r--src/avp/components/sound.py77
-rw-r--r--src/avp/components/sound.ui172
-rw-r--r--src/avp/components/spectrum.py368
-rw-r--r--src/avp/components/spectrum.ui946
-rw-r--r--src/avp/components/text.py218
-rw-r--r--src/avp/components/text.ui671
-rw-r--r--src/avp/components/video.py254
-rw-r--r--src/avp/components/video.ui328
-rw-r--r--src/avp/components/waveform.py230
-rw-r--r--src/avp/components/waveform.ui383
20 files changed, 6537 insertions, 0 deletions
diff --git a/src/avp/components/__init__.py b/src/avp/components/__init__.py
new file mode 100644
index 0000000..8b13789
--- /dev/null
+++ b/src/avp/components/__init__.py
@@ -0,0 +1 @@
+
diff --git a/src/avp/components/__template__.ui b/src/avp/components/__template__.ui
new file mode 100644
index 0000000..301a2b7
--- /dev/null
+++ b/src/avp/components/__template__.ui
@@ -0,0 +1,119 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<ui version="4.0">
+ <class>Form</class>
+ <widget class="QWidget" name="Form">
+ <property name="geometry">
+ <rect>
+ <x>0</x>
+ <y>0</y>
+ <width>586</width>
+ <height>197</height>
+ </rect>
+ </property>
+ <property name="windowTitle">
+ <string>Form</string>
+ </property>
+ <layout class="QVBoxLayout" name="verticalLayout_2">
+ <item>
+ <layout class="QHBoxLayout" name="horizontalLayout_2">
+ <item>
+ <spacer name="horizontalSpacer_2">
+ <property name="orientation">
+ <enum>Qt::Horizontal</enum>
+ </property>
+ <property name="sizeHint" stdset="0">
+ <size>
+ <width>40</width>
+ <height>20</height>
+ </size>
+ </property>
+ </spacer>
+ </item>
+ </layout>
+ </item>
+ <item>
+ <layout class="QHBoxLayout" name="horizontalLayout_3">
+ <item>
+ <spacer name="horizontalSpacer_3">
+ <property name="orientation">
+ <enum>Qt::Horizontal</enum>
+ </property>
+ <property name="sizeHint" stdset="0">
+ <size>
+ <width>40</width>
+ <height>20</height>
+ </size>
+ </property>
+ </spacer>
+ </item>
+ </layout>
+ </item>
+ <item>
+ <layout class="QHBoxLayout" name="horizontalLayout_6">
+ <item>
+ <spacer name="horizontalSpacer_6">
+ <property name="orientation">
+ <enum>Qt::Horizontal</enum>
+ </property>
+ <property name="sizeHint" stdset="0">
+ <size>
+ <width>40</width>
+ <height>20</height>
+ </size>
+ </property>
+ </spacer>
+ </item>
+ </layout>
+ </item>
+ <item>
+ <layout class="QHBoxLayout" name="horizontalLayout_5">
+ <item>
+ <spacer name="horizontalSpacer_5">
+ <property name="orientation">
+ <enum>Qt::Horizontal</enum>
+ </property>
+ <property name="sizeHint" stdset="0">
+ <size>
+ <width>40</width>
+ <height>20</height>
+ </size>
+ </property>
+ </spacer>
+ </item>
+ </layout>
+ </item>
+ <item>
+ <layout class="QHBoxLayout" name="horizontalLayout_4">
+ <item>
+ <spacer name="horizontalSpacer_4">
+ <property name="orientation">
+ <enum>Qt::Horizontal</enum>
+ </property>
+ <property name="sizeHint" stdset="0">
+ <size>
+ <width>40</width>
+ <height>20</height>
+ </size>
+ </property>
+ </spacer>
+ </item>
+ </layout>
+ </item>
+ <item>
+ <spacer name="verticalSpacer">
+ <property name="orientation">
+ <enum>Qt::Vertical</enum>
+ </property>
+ <property name="sizeHint" stdset="0">
+ <size>
+ <width>20</width>
+ <height>40</height>
+ </size>
+ </property>
+ </spacer>
+ </item>
+ </layout>
+ </widget>
+ <resources/>
+ <connections/>
+</ui>
diff --git a/src/avp/components/color.py b/src/avp/components/color.py
new file mode 100644
index 0000000..1f32c23
--- /dev/null
+++ b/src/avp/components/color.py
@@ -0,0 +1,176 @@
+from PyQt6 import QtGui
+import logging
+
+from ..component import Component
+from ..toolkit.frame import BlankFrame, FloodFrame, FramePainter, PaintColor
+
+
+log = logging.getLogger("AVP.Components.Color")
+
+
+class Component(Component):
+ name = "Color"
+ version = "1.0.0"
+
+ def widget(self, *args):
+ self.x = 0
+ self.y = 0
+ super().widget(*args)
+
+ # disable color #2 until non-default 'fill' option gets changed
+ self.page.lineEdit_color2.setDisabled(True)
+ self.page.pushButton_color2.setDisabled(True)
+ self.page.spinBox_width.setValue(int(self.settings.value("outputWidth")))
+ self.page.spinBox_height.setValue(int(self.settings.value("outputHeight")))
+
+ self.fillLabels = [
+ "Solid",
+ "Linear Gradient",
+ "Radial Gradient",
+ ]
+ for label in self.fillLabels:
+ self.page.comboBox_fill.addItem(label)
+ self.page.comboBox_fill.setCurrentIndex(0)
+
+ self.trackWidgets(
+ {
+ "x": self.page.spinBox_x,
+ "y": self.page.spinBox_y,
+ "sizeWidth": self.page.spinBox_width,
+ "sizeHeight": self.page.spinBox_height,
+ "trans": self.page.checkBox_trans,
+ "spread": self.page.comboBox_spread,
+ "stretch": self.page.checkBox_stretch,
+ "RG_start": self.page.spinBox_radialGradient_start,
+ "LG_start": self.page.spinBox_linearGradient_start,
+ "RG_end": self.page.spinBox_radialGradient_end,
+ "LG_end": self.page.spinBox_linearGradient_end,
+ "RG_centre": self.page.spinBox_radialGradient_spread,
+ "fillType": self.page.comboBox_fill,
+ "color1": self.page.lineEdit_color1,
+ "color2": self.page.lineEdit_color2,
+ },
+ presetNames={
+ "sizeWidth": "width",
+ "sizeHeight": "height",
+ },
+ colorWidgets={
+ "color1": self.page.pushButton_color1,
+ "color2": self.page.pushButton_color2,
+ },
+ relativeWidgets=[
+ "x",
+ "y",
+ "sizeWidth",
+ "sizeHeight",
+ "LG_start",
+ "LG_end",
+ "RG_start",
+ "RG_end",
+ "RG_centre",
+ ],
+ )
+
+ def update(self):
+ fillType = self.page.comboBox_fill.currentIndex()
+ if fillType == 0:
+ self.page.lineEdit_color2.setEnabled(False)
+ self.page.pushButton_color2.setEnabled(False)
+ self.page.checkBox_trans.setEnabled(False)
+ self.page.checkBox_stretch.setEnabled(False)
+ self.page.comboBox_spread.setEnabled(False)
+ else:
+ self.page.lineEdit_color2.setEnabled(True)
+ self.page.pushButton_color2.setEnabled(True)
+ self.page.checkBox_trans.setEnabled(True)
+ self.page.checkBox_stretch.setEnabled(True)
+ self.page.comboBox_spread.setEnabled(True)
+ if self.page.checkBox_trans.isChecked():
+ self.page.lineEdit_color2.setEnabled(False)
+ self.page.pushButton_color2.setEnabled(False)
+ self.page.fillWidget.setCurrentIndex(fillType)
+
+ def previewRender(self):
+ return self.drawFrame(self.width, self.height)
+
+ def properties(self):
+ return ["static"]
+
+ def frameRender(self, frameNo):
+ log.debug("Color component is drawing frame #%s", frameNo)
+ return self.drawFrame(self.width, self.height)
+
+ def drawFrame(self, width, height):
+ r, g, b = self.color1
+ shapeSize = (self.sizeWidth, self.sizeHeight)
+ # in default state, skip all this logic and return a plain fill
+ if (
+ self.fillType == 0
+ and shapeSize == (width, height)
+ and self.x == 0
+ and self.y == 0
+ ):
+ return FloodFrame(width, height, (r, g, b, 255))
+
+ # Return a solid image at x, y
+ if self.fillType == 0:
+ frame = BlankFrame(width, height)
+ image = FloodFrame(self.sizeWidth, self.sizeHeight, (r, g, b, 255))
+ frame.paste(image, box=(self.x, self.y))
+ return frame
+
+ # Now fills that require using Qt...
+ elif self.fillType > 0:
+ image = FramePainter(width, height)
+
+ if self.stretch:
+ w = width
+ h = height
+ else:
+ w = self.sizeWidth
+ h = self.sizeWidth
+
+ if self.fillType == 1: # Linear Gradient
+ brush = QtGui.QLinearGradient(
+ float(self.LG_start),
+ float(self.LG_start),
+ float(self.LG_end + width / 3),
+ float(self.LG_end),
+ )
+
+ elif self.fillType == 2: # Radial Gradient
+ brush = QtGui.QRadialGradient(
+ float(self.RG_start),
+ float(self.RG_end),
+ float(w),
+ float(h),
+ float(self.RG_centre),
+ )
+ spread = QtGui.QGradient.Spread.PadSpread
+ if self.spread == 1:
+ spread = QtGui.QGradient.Spread.ReflectSpread
+ elif self.spread == 2:
+ spread = QtGui.QGradient.Spread.RepeatSpread
+ brush.setSpread(spread)
+ brush.setColorAt(0.0, PaintColor(*self.color1))
+ if self.trans:
+ brush.setColorAt(1.0, PaintColor(0, 0, 0, 0))
+ elif self.fillType == 1 and self.stretch:
+ brush.setColorAt(0.2, PaintColor(*self.color2))
+ else:
+ brush.setColorAt(1.0, PaintColor(*self.color2))
+ image.setBrush(brush)
+ image.drawRect(self.x, self.y, self.sizeWidth, self.sizeHeight)
+
+ return image.finalize()
+
+ def commandHelp(self):
+ print("Specify a color:\n color=255,255,255")
+
+ def command(self, arg):
+ if "=" in arg:
+ key, arg = arg.split("=", 1)
+ if key == "color":
+ self.page.lineEdit_color1.setText(arg)
+ return
+ super().command(arg)
diff --git a/src/avp/components/color.ui b/src/avp/components/color.ui
new file mode 100644
index 0000000..c1713fb
--- /dev/null
+++ b/src/avp/components/color.ui
@@ -0,0 +1,666 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<ui version="4.0">
+ <class>Form</class>
+ <widget class="QWidget" name="Form">
+ <property name="geometry">
+ <rect>
+ <x>0</x>
+ <y>0</y>
+ <width>586</width>
+ <height>197</height>
+ </rect>
+ </property>
+ <property name="windowTitle">
+ <string>Form</string>
+ </property>
+ <layout class="QVBoxLayout" name="verticalLayout_2">
+ <item>
+ <layout class="QVBoxLayout" name="verticalLayout">
+ <property name="leftMargin">
+ <number>4</number>
+ </property>
+ <item>
+ <layout class="QHBoxLayout" name="horizontalLayout_8">
+ <item>
+ <widget class="QLabel" name="label_textColor">
+ <property name="sizePolicy">
+ <sizepolicy hsizetype="Fixed" vsizetype="Preferred">
+ <horstretch>0</horstretch>
+ <verstretch>0</verstretch>
+ </sizepolicy>
+ </property>
+ <property name="minimumSize">
+ <size>
+ <width>31</width>
+ <height>0</height>
+ </size>
+ </property>
+ <property name="text">
+ <string>Color #1</string>
+ </property>
+ </widget>
+ </item>
+ <item>
+ <widget class="QPushButton" name="pushButton_color1">
+ <property name="maximumSize">
+ <size>
+ <width>32</width>
+ <height>32</height>
+ </size>
+ </property>
+ <property name="text">
+ <string/>
+ </property>
+ <property name="MaximumSize" stdset="0">
+ <size>
+ <width>32</width>
+ <height>32</height>
+ </size>
+ </property>
+ </widget>
+ </item>
+ <item>
+ <widget class="QLineEdit" name="lineEdit_color1">
+ <property name="sizePolicy">
+ <sizepolicy hsizetype="Fixed" vsizetype="Fixed">
+ <horstretch>0</horstretch>
+ <verstretch>0</verstretch>
+ </sizepolicy>
+ </property>
+ <property name="minimumSize">
+ <size>
+ <width>1</width>
+ <height>0</height>
+ </size>
+ </property>
+ <property name="text">
+ <string>0,0,0</string>
+ </property>
+ <property name="maxLength">
+ <number>12</number>
+ </property>
+ </widget>
+ </item>
+ <item>
+ <spacer name="horizontalSpacer_9">
+ <property name="orientation">
+ <enum>Qt::Horizontal</enum>
+ </property>
+ <property name="sizeType">
+ <enum>QSizePolicy::Fixed</enum>
+ </property>
+ <property name="sizeHint" stdset="0">
+ <size>
+ <width>5</width>
+ <height>20</height>
+ </size>
+ </property>
+ </spacer>
+ </item>
+ <item>
+ <widget class="QLabel" name="label_textColor_2">
+ <property name="sizePolicy">
+ <sizepolicy hsizetype="Fixed" vsizetype="Preferred">
+ <horstretch>0</horstretch>
+ <verstretch>0</verstretch>
+ </sizepolicy>
+ </property>
+ <property name="minimumSize">
+ <size>
+ <width>31</width>
+ <height>0</height>
+ </size>
+ </property>
+ <property name="text">
+ <string>Color #2</string>
+ </property>
+ </widget>
+ </item>
+ <item>
+ <widget class="QPushButton" name="pushButton_color2">
+ <property name="maximumSize">
+ <size>
+ <width>32</width>
+ <height>32</height>
+ </size>
+ </property>
+ <property name="text">
+ <string/>
+ </property>
+ <property name="MaximumSize" stdset="0">
+ <size>
+ <width>32</width>
+ <height>32</height>
+ </size>
+ </property>
+ </widget>
+ </item>
+ <item>
+ <widget class="QLineEdit" name="lineEdit_color2">
+ <property name="sizePolicy">
+ <sizepolicy hsizetype="Minimum" vsizetype="Fixed">
+ <horstretch>0</horstretch>
+ <verstretch>0</verstretch>
+ </sizepolicy>
+ </property>
+ <property name="minimumSize">
+ <size>
+ <width>1</width>
+ <height>0</height>
+ </size>
+ </property>
+ <property name="text">
+ <string>133,133,133</string>
+ </property>
+ <property name="maxLength">
+ <number>12</number>
+ </property>
+ </widget>
+ </item>
+ </layout>
+ </item>
+ <item>
+ <layout class="QHBoxLayout" name="horizontalLayout_7">
+ <property name="leftMargin">
+ <number>0</number>
+ </property>
+ <item>
+ <widget class="QLabel" name="label_xTitleAlign_2">
+ <property name="sizePolicy">
+ <sizepolicy hsizetype="Fixed" vsizetype="Preferred">
+ <horstretch>0</horstretch>
+ <verstretch>0</verstretch>
+ </sizepolicy>
+ </property>
+ <property name="text">
+ <string>Width</string>
+ </property>
+ <property name="alignment">
+ <set>Qt::AlignLeading|Qt::AlignLeft|Qt::AlignVCenter</set>
+ </property>
+ </widget>
+ </item>
+ <item>
+ <widget class="QSpinBox" name="spinBox_width">
+ <property name="sizePolicy">
+ <sizepolicy hsizetype="Fixed" vsizetype="Fixed">
+ <horstretch>0</horstretch>
+ <verstretch>0</verstretch>
+ </sizepolicy>
+ </property>
+ <property name="maximumSize">
+ <size>
+ <width>80</width>
+ <height>16777215</height>
+ </size>
+ </property>
+ <property name="baseSize">
+ <size>
+ <width>0</width>
+ <height>0</height>
+ </size>
+ </property>
+ <property name="minimum">
+ <number>0</number>
+ </property>
+ <property name="maximum">
+ <number>19200</number>
+ </property>
+ <property name="value">
+ <number>0</number>
+ </property>
+ </widget>
+ </item>
+ <item>
+ <widget class="QLabel" name="label_yTitleAlign_2">
+ <property name="sizePolicy">
+ <sizepolicy hsizetype="Fixed" vsizetype="Preferred">
+ <horstretch>0</horstretch>
+ <verstretch>0</verstretch>
+ </sizepolicy>
+ </property>
+ <property name="text">
+ <string>Height</string>
+ </property>
+ </widget>
+ </item>
+ <item>
+ <widget class="QSpinBox" name="spinBox_height">
+ <property name="sizePolicy">
+ <sizepolicy hsizetype="Fixed" vsizetype="Fixed">
+ <horstretch>0</horstretch>
+ <verstretch>0</verstretch>
+ </sizepolicy>
+ </property>
+ <property name="maximumSize">
+ <size>
+ <width>80</width>
+ <height>16777215</height>
+ </size>
+ </property>
+ <property name="maximum">
+ <number>10800</number>
+ </property>
+ </widget>
+ </item>
+ <item>
+ <spacer name="horizontalSpacer_7">
+ <property name="orientation">
+ <enum>Qt::Horizontal</enum>
+ </property>
+ <property name="sizeType">
+ <enum>QSizePolicy::Fixed</enum>
+ </property>
+ <property name="sizeHint" stdset="0">
+ <size>
+ <width>5</width>
+ <height>20</height>
+ </size>
+ </property>
+ </spacer>
+ </item>
+ <item>
+ <widget class="QLabel" name="label_xTitleAlign">
+ <property name="sizePolicy">
+ <sizepolicy hsizetype="Fixed" vsizetype="Preferred">
+ <horstretch>0</horstretch>
+ <verstretch>0</verstretch>
+ </sizepolicy>
+ </property>
+ <property name="text">
+ <string>X</string>
+ </property>
+ </widget>
+ </item>
+ <item>
+ <widget class="QSpinBox" name="spinBox_x">
+ <property name="sizePolicy">
+ <sizepolicy hsizetype="Fixed" vsizetype="Fixed">
+ <horstretch>0</horstretch>
+ <verstretch>0</verstretch>
+ </sizepolicy>
+ </property>
+ <property name="maximumSize">
+ <size>
+ <width>80</width>
+ <height>16777215</height>
+ </size>
+ </property>
+ <property name="baseSize">
+ <size>
+ <width>0</width>
+ <height>0</height>
+ </size>
+ </property>
+ <property name="minimum">
+ <number>-10000</number>
+ </property>
+ <property name="maximum">
+ <number>10000</number>
+ </property>
+ <property name="value">
+ <number>0</number>
+ </property>
+ </widget>
+ </item>
+ <item>
+ <widget class="QLabel" name="label_yTitleAlign">
+ <property name="sizePolicy">
+ <sizepolicy hsizetype="Fixed" vsizetype="Preferred">
+ <horstretch>0</horstretch>
+ <verstretch>0</verstretch>
+ </sizepolicy>
+ </property>
+ <property name="text">
+ <string>Y</string>
+ </property>
+ </widget>
+ </item>
+ <item>
+ <widget class="QSpinBox" name="spinBox_y">
+ <property name="sizePolicy">
+ <sizepolicy hsizetype="Fixed" vsizetype="Fixed">
+ <horstretch>0</horstretch>
+ <verstretch>0</verstretch>
+ </sizepolicy>
+ </property>
+ <property name="maximumSize">
+ <size>
+ <width>80</width>
+ <height>16777215</height>
+ </size>
+ </property>
+ <property name="minimum">
+ <number>-10000</number>
+ </property>
+ <property name="maximum">
+ <number>10000</number>
+ </property>
+ </widget>
+ </item>
+ </layout>
+ </item>
+ <item>
+ <layout class="QHBoxLayout" name="horizontalLayout_9">
+ <property name="leftMargin">
+ <number>0</number>
+ </property>
+ <item>
+ <widget class="QLabel" name="label_textLayout">
+ <property name="sizePolicy">
+ <sizepolicy hsizetype="Fixed" vsizetype="Preferred">
+ <horstretch>0</horstretch>
+ <verstretch>0</verstretch>
+ </sizepolicy>
+ </property>
+ <property name="text">
+ <string>Fill </string>
+ </property>
+ </widget>
+ </item>
+ <item>
+ <widget class="QComboBox" name="comboBox_fill">
+ <property name="sizePolicy">
+ <sizepolicy hsizetype="Minimum" vsizetype="Fixed">
+ <horstretch>0</horstretch>
+ <verstretch>0</verstretch>
+ </sizepolicy>
+ </property>
+ <property name="currentIndex">
+ <number>-1</number>
+ </property>
+ <property name="sizeAdjustPolicy">
+ <enum>QComboBox::AdjustToContentsOnFirstShow</enum>
+ </property>
+ </widget>
+ </item>
+ <item>
+ <widget class="QCheckBox" name="checkBox_trans">
+ <property name="sizePolicy">
+ <sizepolicy hsizetype="Fixed" vsizetype="Fixed">
+ <horstretch>0</horstretch>
+ <verstretch>0</verstretch>
+ </sizepolicy>
+ </property>
+ <property name="text">
+ <string>Transparent</string>
+ </property>
+ </widget>
+ </item>
+ <item>
+ <widget class="QCheckBox" name="checkBox_stretch">
+ <property name="sizePolicy">
+ <sizepolicy hsizetype="Fixed" vsizetype="Fixed">
+ <horstretch>0</horstretch>
+ <verstretch>0</verstretch>
+ </sizepolicy>
+ </property>
+ <property name="text">
+ <string>Stretch</string>
+ </property>
+ </widget>
+ </item>
+ <item>
+ <widget class="QComboBox" name="comboBox_spread">
+ <item>
+ <property name="text">
+ <string>Pad</string>
+ </property>
+ </item>
+ <item>
+ <property name="text">
+ <string>Reflect</string>
+ </property>
+ </item>
+ <item>
+ <property name="text">
+ <string>Repeat</string>
+ </property>
+ </item>
+ </widget>
+ </item>
+ <item>
+ <spacer name="horizontalSpacer_2">
+ <property name="orientation">
+ <enum>Qt::Horizontal</enum>
+ </property>
+ <property name="sizeType">
+ <enum>QSizePolicy::Minimum</enum>
+ </property>
+ <property name="sizeHint" stdset="0">
+ <size>
+ <width>40</width>
+ <height>20</height>
+ </size>
+ </property>
+ </spacer>
+ </item>
+ </layout>
+ </item>
+ <item>
+ <layout class="QHBoxLayout" name="horizontalLayout_2">
+ <item>
+ <widget class="QStackedWidget" name="fillWidget">
+ <property name="sizePolicy">
+ <sizepolicy hsizetype="Preferred" vsizetype="Minimum">
+ <horstretch>0</horstretch>
+ <verstretch>0</verstretch>
+ </sizepolicy>
+ </property>
+ <property name="lineWidth">
+ <number>0</number>
+ </property>
+ <property name="currentIndex">
+ <number>2</number>
+ </property>
+ <widget class="QWidget" name="blank"/>
+ <widget class="QWidget" name="linearGradient">
+ <widget class="QWidget" name="horizontalLayoutWidget">
+ <property name="geometry">
+ <rect>
+ <x>-1</x>
+ <y>0</y>
+ <width>561</width>
+ <height>31</height>
+ </rect>
+ </property>
+ <layout class="QHBoxLayout" name="horizontalLayout">
+ <item>
+ <widget class="QLabel" name="label_xTitleAlign_4">
+ <property name="sizePolicy">
+ <sizepolicy hsizetype="Fixed" vsizetype="Preferred">
+ <horstretch>0</horstretch>
+ <verstretch>0</verstretch>
+ </sizepolicy>
+ </property>
+ <property name="text">
+ <string>Start</string>
+ </property>
+ </widget>
+ </item>
+ <item>
+ <widget class="QSpinBox" name="spinBox_linearGradient_start">
+ <property name="minimum">
+ <number>-10000</number>
+ </property>
+ <property name="maximum">
+ <number>10000</number>
+ </property>
+ <property name="singleStep">
+ <number>10</number>
+ </property>
+ </widget>
+ </item>
+ <item>
+ <widget class="QLabel" name="label">
+ <property name="sizePolicy">
+ <sizepolicy hsizetype="Fixed" vsizetype="Preferred">
+ <horstretch>0</horstretch>
+ <verstretch>0</verstretch>
+ </sizepolicy>
+ </property>
+ <property name="text">
+ <string>End</string>
+ </property>
+ <property name="alignment">
+ <set>Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter</set>
+ </property>
+ </widget>
+ </item>
+ <item>
+ <widget class="QSpinBox" name="spinBox_linearGradient_end">
+ <property name="minimum">
+ <number>-10000</number>
+ </property>
+ <property name="maximum">
+ <number>10000</number>
+ </property>
+ <property name="singleStep">
+ <number>10</number>
+ </property>
+ </widget>
+ </item>
+ <item>
+ <spacer name="horizontalSpacer">
+ <property name="orientation">
+ <enum>Qt::Horizontal</enum>
+ </property>
+ <property name="sizeHint" stdset="0">
+ <size>
+ <width>40</width>
+ <height>20</height>
+ </size>
+ </property>
+ </spacer>
+ </item>
+ </layout>
+ </widget>
+ </widget>
+ <widget class="QWidget" name="radialGradient">
+ <widget class="QWidget" name="horizontalLayoutWidget_3">
+ <property name="geometry">
+ <rect>
+ <x>-1</x>
+ <y>-1</y>
+ <width>561</width>
+ <height>31</height>
+ </rect>
+ </property>
+ <layout class="QHBoxLayout" name="horizontalLayout_3">
+ <item>
+ <widget class="QLabel" name="label_xTitleAlign_6">
+ <property name="sizePolicy">
+ <sizepolicy hsizetype="Fixed" vsizetype="Preferred">
+ <horstretch>0</horstretch>
+ <verstretch>0</verstretch>
+ </sizepolicy>
+ </property>
+ <property name="text">
+ <string>Start</string>
+ </property>
+ <property name="alignment">
+ <set>Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter</set>
+ </property>
+ </widget>
+ </item>
+ <item>
+ <widget class="QSpinBox" name="spinBox_radialGradient_start">
+ <property name="minimum">
+ <number>-10000</number>
+ </property>
+ <property name="maximum">
+ <number>10000</number>
+ </property>
+ <property name="singleStep">
+ <number>10</number>
+ </property>
+ </widget>
+ </item>
+ <item>
+ <widget class="QLabel" name="label_3">
+ <property name="sizePolicy">
+ <sizepolicy hsizetype="Fixed" vsizetype="Preferred">
+ <horstretch>0</horstretch>
+ <verstretch>0</verstretch>
+ </sizepolicy>
+ </property>
+ <property name="text">
+ <string>End</string>
+ </property>
+ <property name="alignment">
+ <set>Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter</set>
+ </property>
+ </widget>
+ </item>
+ <item>
+ <widget class="QSpinBox" name="spinBox_radialGradient_end">
+ <property name="minimum">
+ <number>-10000</number>
+ </property>
+ <property name="maximum">
+ <number>10000</number>
+ </property>
+ <property name="singleStep">
+ <number>10</number>
+ </property>
+ </widget>
+ </item>
+ <item>
+ <widget class="QLabel" name="label_4">
+ <property name="sizePolicy">
+ <sizepolicy hsizetype="Fixed" vsizetype="Preferred">
+ <horstretch>0</horstretch>
+ <verstretch>0</verstretch>
+ </sizepolicy>
+ </property>
+ <property name="text">
+ <string>Centre</string>
+ </property>
+ <property name="alignment">
+ <set>Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter</set>
+ </property>
+ </widget>
+ </item>
+ <item>
+ <widget class="QSpinBox" name="spinBox_radialGradient_spread">
+ <property name="buttonSymbols">
+ <enum>QAbstractSpinBox::PlusMinus</enum>
+ </property>
+ <property name="minimum">
+ <number>-10000</number>
+ </property>
+ <property name="maximum">
+ <number>10000</number>
+ </property>
+ <property name="value">
+ <number>3</number>
+ </property>
+ </widget>
+ </item>
+ <item>
+ <spacer name="horizontalSpacer_3">
+ <property name="orientation">
+ <enum>Qt::Horizontal</enum>
+ </property>
+ <property name="sizeHint" stdset="0">
+ <size>
+ <width>40</width>
+ <height>20</height>
+ </size>
+ </property>
+ </spacer>
+ </item>
+ </layout>
+ </widget>
+ </widget>
+ </widget>
+ </item>
+ </layout>
+ </item>
+ </layout>
+ </item>
+ </layout>
+ </widget>
+ <resources/>
+ <connections/>
+</ui>
diff --git a/src/avp/components/image.py b/src/avp/components/image.py
new file mode 100644
index 0000000..2393611
--- /dev/null
+++ b/src/avp/components/image.py
@@ -0,0 +1,129 @@
+from PIL import Image, ImageDraw, ImageEnhance
+from PyQt6 import QtGui, QtCore, QtWidgets
+import os
+
+from ..component import Component
+from ..toolkit.frame import BlankFrame
+
+
+class Component(Component):
+ name = "Image"
+ version = "1.0.1"
+
+ def widget(self, *args):
+ super().widget(*args)
+ self.page.pushButton_image.clicked.connect(self.pickImage)
+ self.trackWidgets(
+ {
+ "imagePath": self.page.lineEdit_image,
+ "scale": self.page.spinBox_scale,
+ "stretchScale": self.page.spinBox_scale_stretch,
+ "rotate": self.page.spinBox_rotate,
+ "color": self.page.spinBox_color,
+ "xPosition": self.page.spinBox_x,
+ "yPosition": self.page.spinBox_y,
+ "stretched": self.page.checkBox_stretch,
+ "mirror": self.page.checkBox_mirror,
+ },
+ presetNames={
+ "imagePath": "image",
+ "xPosition": "x",
+ "yPosition": "y",
+ },
+ relativeWidgets=["xPosition", "yPosition", "scale"],
+ )
+
+ def previewRender(self):
+ return self.drawFrame(self.width, self.height)
+
+ def properties(self):
+ props = ["static"]
+ if not os.path.exists(self.imagePath):
+ props.append("error")
+ return props
+
+ def error(self):
+ if not self.imagePath:
+ return "There is no image selected."
+ if not os.path.exists(self.imagePath):
+ return "The image selected does not exist!"
+
+ def frameRender(self, frameNo):
+ return self.drawFrame(self.width, self.height)
+
+ def drawFrame(self, width, height):
+ frame = BlankFrame(width, height)
+ if self.imagePath and os.path.exists(self.imagePath):
+ scale = self.scale if not self.stretched else self.stretchScale
+ image = Image.open(self.imagePath)
+
+ # Modify image's appearance
+ if self.color != 100:
+ image = ImageEnhance.Color(image).enhance(float(self.color / 100))
+ if self.mirror:
+ image = image.transpose(Image.Transpose.FLIP_LEFT_RIGHT)
+ if self.stretched and image.size != (width, height):
+ image = image.resize((width, height), Image.Resampling.LANCZOS)
+ if scale != 100:
+ newHeight = int((image.height / 100) * scale)
+ newWidth = int((image.width / 100) * scale)
+ image = image.resize((newWidth, newHeight), Image.Resampling.LANCZOS)
+
+ # Paste image at correct position
+ frame.paste(image, box=(self.xPosition, self.yPosition))
+ if self.rotate != 0:
+ frame = frame.rotate(self.rotate)
+
+ return frame
+
+ def pickImage(self):
+ imgDir = self.settings.value("componentDir", os.path.expanduser("~"))
+ filename, _ = QtWidgets.QFileDialog.getOpenFileName(
+ self.page,
+ "Choose Image",
+ imgDir,
+ "Image Files (%s)" % " ".join(self.core.imageFormats),
+ )
+ if filename:
+ self.settings.setValue("componentDir", os.path.dirname(filename))
+ self.mergeUndo = False
+ self.page.lineEdit_image.setText(filename)
+ self.mergeUndo = True
+
+ def command(self, arg):
+ if "=" in arg:
+ key, arg = arg.split("=", 1)
+ if key == "path" and os.path.exists(arg):
+ try:
+ Image.open(arg)
+ self.page.lineEdit_image.setText(arg)
+ self.page.checkBox_stretch.setChecked(True)
+ return
+ except OSError as e:
+ print("Not a supported image format")
+ quit(1)
+ super().command(arg)
+
+ def commandHelp(self):
+ print("Load an image:\n path=/filepath/to/image.png")
+
+ def savePreset(self):
+ # Maintain the illusion that the scale spinbox is one widget
+ scaleBox = self.page.spinBox_scale
+ stretchScaleBox = self.page.spinBox_scale_stretch
+ if self.page.checkBox_stretch.isChecked():
+ scaleBox.setValue(stretchScaleBox.value())
+ else:
+ stretchScaleBox.setValue(scaleBox.value())
+ return super().savePreset()
+
+ def update(self):
+ # Maintain the illusion that the scale spinbox is one widget
+ scaleBox = self.page.spinBox_scale
+ stretchScaleBox = self.page.spinBox_scale_stretch
+ if self.page.checkBox_stretch.isChecked():
+ scaleBox.setVisible(False)
+ stretchScaleBox.setVisible(True)
+ else:
+ scaleBox.setVisible(True)
+ stretchScaleBox.setVisible(False)
diff --git a/src/avp/components/image.ui b/src/avp/components/image.ui
new file mode 100644
index 0000000..2dad127
--- /dev/null
+++ b/src/avp/components/image.ui
@@ -0,0 +1,388 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<ui version="4.0">
+ <class>Form</class>
+ <widget class="QWidget" name="Form">
+ <property name="geometry">
+ <rect>
+ <x>0</x>
+ <y>0</y>
+ <width>586</width>
+ <height>197</height>
+ </rect>
+ </property>
+ <property name="windowTitle">
+ <string>Form</string>
+ </property>
+ <layout class="QVBoxLayout" name="verticalLayout_2">
+ <item>
+ <layout class="QVBoxLayout" name="verticalLayout">
+ <property name="leftMargin">
+ <number>4</number>
+ </property>
+ <item>
+ <layout class="QHBoxLayout" name="horizontalLayout_8">
+ <item>
+ <widget class="QLabel" name="label_textColor">
+ <property name="sizePolicy">
+ <sizepolicy hsizetype="Fixed" vsizetype="Preferred">
+ <horstretch>0</horstretch>
+ <verstretch>0</verstretch>
+ </sizepolicy>
+ </property>
+ <property name="minimumSize">
+ <size>
+ <width>31</width>
+ <height>0</height>
+ </size>
+ </property>
+ <property name="text">
+ <string>Image</string>
+ </property>
+ </widget>
+ </item>
+ <item>
+ <widget class="QLineEdit" name="lineEdit_image">
+ <property name="minimumSize">
+ <size>
+ <width>1</width>
+ <height>0</height>
+ </size>
+ </property>
+ </widget>
+ </item>
+ <item>
+ <widget class="QPushButton" name="pushButton_image">
+ <property name="sizePolicy">
+ <sizepolicy hsizetype="Fixed" vsizetype="Fixed">
+ <horstretch>0</horstretch>
+ <verstretch>0</verstretch>
+ </sizepolicy>
+ </property>
+ <property name="minimumSize">
+ <size>
+ <width>1</width>
+ <height>0</height>
+ </size>
+ </property>
+ <property name="maximumSize">
+ <size>
+ <width>32</width>
+ <height>32</height>
+ </size>
+ </property>
+ <property name="text">
+ <string>...</string>
+ </property>
+ <property name="MaximumSize" stdset="0">
+ <size>
+ <width>32</width>
+ <height>32</height>
+ </size>
+ </property>
+ </widget>
+ </item>
+ <item>
+ <spacer name="horizontalSpacer_9">
+ <property name="orientation">
+ <enum>Qt::Horizontal</enum>
+ </property>
+ <property name="sizeType">
+ <enum>QSizePolicy::Fixed</enum>
+ </property>
+ <property name="sizeHint" stdset="0">
+ <size>
+ <width>5</width>
+ <height>20</height>
+ </size>
+ </property>
+ </spacer>
+ </item>
+ <item>
+ <widget class="QLabel" name="label_xTitleAlign">
+ <property name="sizePolicy">
+ <sizepolicy hsizetype="Fixed" vsizetype="Preferred">
+ <horstretch>0</horstretch>
+ <verstretch>0</verstretch>
+ </sizepolicy>
+ </property>
+ <property name="text">
+ <string>X</string>
+ </property>
+ </widget>
+ </item>
+ <item>
+ <widget class="QSpinBox" name="spinBox_x">
+ <property name="sizePolicy">
+ <sizepolicy hsizetype="Fixed" vsizetype="Fixed">
+ <horstretch>0</horstretch>
+ <verstretch>0</verstretch>
+ </sizepolicy>
+ </property>
+ <property name="maximumSize">
+ <size>
+ <width>80</width>
+ <height>16777215</height>
+ </size>
+ </property>
+ <property name="minimum">
+ <number>-10000</number>
+ </property>
+ <property name="maximum">
+ <number>10000</number>
+ </property>
+ </widget>
+ </item>
+ <item>
+ <widget class="QLabel" name="label_yTitleAlign">
+ <property name="sizePolicy">
+ <sizepolicy hsizetype="Fixed" vsizetype="Preferred">
+ <horstretch>0</horstretch>
+ <verstretch>0</verstretch>
+ </sizepolicy>
+ </property>
+ <property name="text">
+ <string>Y</string>
+ </property>
+ </widget>
+ </item>
+ <item>
+ <widget class="QSpinBox" name="spinBox_y">
+ <property name="sizePolicy">
+ <sizepolicy hsizetype="Fixed" vsizetype="Fixed">
+ <horstretch>0</horstretch>
+ <verstretch>0</verstretch>
+ </sizepolicy>
+ </property>
+ <property name="maximumSize">
+ <size>
+ <width>80</width>
+ <height>16777215</height>
+ </size>
+ </property>
+ <property name="baseSize">
+ <size>
+ <width>0</width>
+ <height>0</height>
+ </size>
+ </property>
+ <property name="minimum">
+ <number>-1000</number>
+ </property>
+ <property name="maximum">
+ <number>1000</number>
+ </property>
+ <property name="value">
+ <number>0</number>
+ </property>
+ </widget>
+ </item>
+ </layout>
+ </item>
+ <item>
+ <layout class="QHBoxLayout" name="horizontalLayout_9">
+ <item>
+ <widget class="QCheckBox" name="checkBox_stretch">
+ <property name="text">
+ <string>Stretch</string>
+ </property>
+ <property name="checked">
+ <bool>false</bool>
+ </property>
+ </widget>
+ </item>
+ <item>
+ <spacer name="horizontalSpacer_10">
+ <property name="orientation">
+ <enum>Qt::Horizontal</enum>
+ </property>
+ <property name="sizeType">
+ <enum>QSizePolicy::Fixed</enum>
+ </property>
+ <property name="sizeHint" stdset="0">
+ <size>
+ <width>5</width>
+ <height>20</height>
+ </size>
+ </property>
+ </spacer>
+ </item>
+ <item>
+ <widget class="QCheckBox" name="checkBox_mirror">
+ <property name="text">
+ <string>Mirror</string>
+ </property>
+ </widget>
+ </item>
+ <item>
+ <widget class="QLabel" name="label_2">
+ <property name="text">
+ <string>Rotate</string>
+ </property>
+ <property name="alignment">
+ <set>Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter</set>
+ </property>
+ </widget>
+ </item>
+ <item>
+ <widget class="QSpinBox" name="spinBox_rotate">
+ <property name="buttonSymbols">
+ <enum>QAbstractSpinBox::UpDownArrows</enum>
+ </property>
+ <property name="suffix">
+ <string notr="true">°</string>
+ </property>
+ <property name="minimum">
+ <number>0</number>
+ </property>
+ <property name="maximum">
+ <number>359</number>
+ </property>
+ <property name="value">
+ <number>0</number>
+ </property>
+ </widget>
+ </item>
+ <item>
+ <spacer name="horizontalSpacer">
+ <property name="orientation">
+ <enum>Qt::Horizontal</enum>
+ </property>
+ <property name="sizeType">
+ <enum>QSizePolicy::Fixed</enum>
+ </property>
+ <property name="sizeHint" stdset="0">
+ <size>
+ <width>10</width>
+ <height>20</height>
+ </size>
+ </property>
+ </spacer>
+ </item>
+ <item>
+ <widget class="QLabel" name="label">
+ <property name="sizePolicy">
+ <sizepolicy hsizetype="Fixed" vsizetype="Preferred">
+ <horstretch>0</horstretch>
+ <verstretch>0</verstretch>
+ </sizepolicy>
+ </property>
+ <property name="text">
+ <string>Scale</string>
+ </property>
+ <property name="alignment">
+ <set>Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter</set>
+ </property>
+ </widget>
+ </item>
+ <item>
+ <widget class="QSpinBox" name="spinBox_scale">
+ <property name="buttonSymbols">
+ <enum>QAbstractSpinBox::UpDownArrows</enum>
+ </property>
+ <property name="suffix">
+ <string>%</string>
+ </property>
+ <property name="minimum">
+ <number>10</number>
+ </property>
+ <property name="maximum">
+ <number>400</number>
+ </property>
+ <property name="value">
+ <number>100</number>
+ </property>
+ </widget>
+ </item>
+ <item>
+ <widget class="QSpinBox" name="spinBox_scale_stretch">
+ <property name="suffix">
+ <string>%</string>
+ </property>
+ <property name="minimum">
+ <number>10</number>
+ </property>
+ <property name="maximum">
+ <number>400</number>
+ </property>
+ <property name="value">
+ <number>100</number>
+ </property>
+ </widget>
+ </item>
+ </layout>
+ </item>
+ <item>
+ <layout class="QHBoxLayout" name="horizontalLayout_2">
+ <item>
+ <spacer name="horizontalSpacer_2">
+ <property name="orientation">
+ <enum>Qt::Horizontal</enum>
+ </property>
+ <property name="sizeHint" stdset="0">
+ <size>
+ <width>40</width>
+ <height>20</height>
+ </size>
+ </property>
+ </spacer>
+ </item>
+ <item>
+ <widget class="QLabel" name="label_3">
+ <property name="sizePolicy">
+ <sizepolicy hsizetype="Fixed" vsizetype="Preferred">
+ <horstretch>0</horstretch>
+ <verstretch>0</verstretch>
+ </sizepolicy>
+ </property>
+ <property name="text">
+ <string>Color</string>
+ </property>
+ <property name="alignment">
+ <set>Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter</set>
+ </property>
+ </widget>
+ </item>
+ <item>
+ <widget class="QSpinBox" name="spinBox_color">
+ <property name="buttonSymbols">
+ <enum>QAbstractSpinBox::UpDownArrows</enum>
+ </property>
+ <property name="suffix">
+ <string>%</string>
+ </property>
+ <property name="minimum">
+ <number>0</number>
+ </property>
+ <property name="maximum">
+ <number>999</number>
+ </property>
+ <property name="singleStep">
+ <number>1</number>
+ </property>
+ <property name="value">
+ <number>100</number>
+ </property>
+ </widget>
+ </item>
+ </layout>
+ </item>
+ </layout>
+ </item>
+ <item>
+ <spacer name="verticalSpacer">
+ <property name="orientation">
+ <enum>Qt::Vertical</enum>
+ </property>
+ <property name="sizeHint" stdset="0">
+ <size>
+ <width>20</width>
+ <height>40</height>
+ </size>
+ </property>
+ </spacer>
+ </item>
+ </layout>
+ </widget>
+ <resources/>
+ <connections/>
+</ui>
diff --git a/src/avp/components/life.py b/src/avp/components/life.py
new file mode 100644
index 0000000..5b719d1
--- /dev/null
+++ b/src/avp/components/life.py
@@ -0,0 +1,520 @@
+from PyQt6 import QtGui, QtCore, QtWidgets
+from PyQt6.QtGui import QUndoCommand
+from PIL import Image, ImageDraw, ImageEnhance, ImageChops, ImageFilter
+import os
+import math
+import logging
+
+
+from ..component import Component
+from ..toolkit.frame import BlankFrame, scale
+
+
+log = logging.getLogger("AVP.Component.Life")
+
+
+class Component(Component):
+ name = "Conway's Game of Life"
+ version = "1.0.0"
+
+ def widget(self, *args):
+ super().widget(*args)
+ self.scale = 32
+ self.updateGridSize()
+ # The initial grid: a "Queen Bee Shuttle"
+ # https://conwaylife.com/wiki/Queen_bee_shuttle
+ self.startingGrid = set(
+ [
+ (3, 7),
+ (3, 8),
+ (4, 7),
+ (4, 8),
+ (8, 7),
+ (9, 6),
+ (9, 8),
+ (10, 5),
+ (10, 9),
+ (11, 6),
+ (11, 7),
+ (11, 8),
+ (12, 4),
+ (12, 5),
+ (12, 9),
+ (12, 10),
+ (23, 6),
+ (23, 7),
+ (24, 6),
+ (24, 7),
+ ]
+ )
+
+ # Amount of 'bleed' (off-canvas coordinates) on each side of the grid
+ self.bleedSize = 40
+
+ self.page.pushButton_pickImage.clicked.connect(self.pickImage)
+ self.trackWidgets(
+ {
+ "tickRate": self.page.spinBox_tickRate,
+ "scale": self.page.spinBox_scale,
+ "color": self.page.lineEdit_color,
+ "shapeType": self.page.comboBox_shapeType,
+ "shadow": self.page.checkBox_shadow,
+ "customImg": self.page.checkBox_customImg,
+ "showGrid": self.page.checkBox_showGrid,
+ "image": self.page.lineEdit_image,
+ },
+ colorWidgets={
+ "color": self.page.pushButton_color,
+ },
+ )
+ self.shiftButtons = (
+ self.page.toolButton_up,
+ self.page.toolButton_down,
+ self.page.toolButton_left,
+ self.page.toolButton_right,
+ )
+
+ def shiftFunc(i):
+ def shift():
+ self.shiftGrid(i)
+
+ return shift
+
+ shiftFuncs = [shiftFunc(i) for i in range(len(self.shiftButtons))]
+ for i, widget in enumerate(self.shiftButtons):
+ widget.clicked.connect(shiftFuncs[i])
+ self.page.spinBox_scale.setValue(self.scale)
+ self.page.spinBox_scale.valueChanged.connect(self.updateGridSize)
+
+ def pickImage(self):
+ imgDir = self.settings.value("componentDir", os.path.expanduser("~"))
+ filename, _ = QtWidgets.QFileDialog.getOpenFileName(
+ self.page,
+ "Choose Image",
+ imgDir,
+ "Image Files (%s)" % " ".join(self.core.imageFormats),
+ )
+ if filename:
+ self.settings.setValue("componentDir", os.path.dirname(filename))
+ self.mergeUndo = False
+ self.page.lineEdit_image.setText(filename)
+ self.mergeUndo = True
+
+ def shiftGrid(self, d):
+ action = ShiftGrid(self, d)
+ self.parent.undoStack.push(action)
+
+ def update(self):
+ self.updateGridSize()
+ if self.page.checkBox_customImg.isChecked():
+ self.page.label_color.setVisible(False)
+ self.page.lineEdit_color.setVisible(False)
+ self.page.pushButton_color.setVisible(False)
+ self.page.label_shape.setVisible(False)
+ self.page.comboBox_shapeType.setVisible(False)
+ self.page.label_image.setVisible(True)
+ self.page.lineEdit_image.setVisible(True)
+ self.page.pushButton_pickImage.setVisible(True)
+ else:
+ self.page.label_color.setVisible(True)
+ self.page.lineEdit_color.setVisible(True)
+ self.page.pushButton_color.setVisible(True)
+ self.page.label_shape.setVisible(True)
+ self.page.comboBox_shapeType.setVisible(True)
+ self.page.label_image.setVisible(False)
+ self.page.lineEdit_image.setVisible(False)
+ self.page.pushButton_pickImage.setVisible(False)
+ enabled = len(self.startingGrid) > 0
+ for widget in self.shiftButtons:
+ widget.setEnabled(enabled)
+
+ def previewClickEvent(self, pos, size, button):
+ pos = (
+ math.ceil((pos[0] / size[0]) * self.gridWidth) - 1,
+ math.ceil((pos[1] / size[1]) * self.gridHeight) - 1,
+ )
+ action = ClickGrid(self, pos, button)
+ self.parent.undoStack.push(action)
+
+ def updateGridSize(self):
+ w, h = self.core.resolutions[-1].split("x")
+ self.gridWidth = int(int(w) / self.scale)
+ self.gridHeight = int(int(h) / self.scale)
+ self.pxWidth = math.ceil(self.width / self.gridWidth)
+ self.pxHeight = math.ceil(self.height / self.gridHeight)
+
+ def previewRender(self):
+ return self.drawGrid(self.startingGrid)
+
+ def preFrameRender(self, *args, **kwargs):
+ super().preFrameRender(*args, **kwargs)
+ self.tickGrids = {0: self.startingGrid}
+
+ def properties(self):
+ if self.customImg and (not self.image or not os.path.exists(self.image)):
+ return ["error"]
+ return []
+
+ def error(self):
+ return "No image selected to represent life."
+
+ def frameRender(self, frameNo):
+ tick = math.floor(frameNo / self.tickRate)
+
+ # Compute grid evolution on this frame if it hasn't been computed yet
+ if tick not in self.tickGrids:
+ self.tickGrids[tick] = self.gridForTick(tick)
+ grid = self.tickGrids[tick]
+
+ # Delete old evolution data which we shouldn't need anymore
+ if tick - 60 in self.tickGrids:
+ del self.tickGrids[tick - 60]
+ return self.drawGrid(grid)
+
+ def drawGrid(self, grid):
+ frame = BlankFrame(self.width, self.height)
+
+ def drawCustomImg():
+ try:
+ img = Image.open(self.image)
+ except Exception:
+ return
+ img = img.resize((self.pxWidth, self.pxHeight), Image.Resampling.LANCZOS)
+ frame.paste(img, box=(drawPtX, drawPtY))
+
+ def drawShape():
+ drawer = ImageDraw.Draw(frame)
+ rect = (
+ (drawPtX, drawPtY),
+ (drawPtX + self.pxWidth, drawPtY + self.pxHeight),
+ )
+ shape = self.page.comboBox_shapeType.currentText().lower()
+
+ # Rectangle
+ if shape == "rectangle":
+ drawer.rectangle(rect, fill=self.color)
+
+ # Elliptical
+ elif shape == "elliptical":
+ drawer.ellipse(rect, fill=self.color)
+
+ tenthX, tenthY = scale(10, self.pxWidth, self.pxHeight, int)
+ smallerShape = (
+ (
+ drawPtX + tenthX + int(tenthX / 4),
+ drawPtY + tenthY + int(tenthY / 2),
+ ),
+ (
+ drawPtX + self.pxWidth - tenthX - int(tenthX / 4),
+ drawPtY + self.pxHeight - (tenthY + int(tenthY / 2)),
+ ),
+ )
+ outlineShape = (
+ (drawPtX + int(tenthX / 4), drawPtY + int(tenthY / 2)),
+ (
+ drawPtX + self.pxWidth - int(tenthX / 4),
+ drawPtY + self.pxHeight - int(tenthY / 2),
+ ),
+ )
+ # Circle
+ if shape == "circle":
+ drawer.ellipse(outlineShape, fill=self.color)
+ drawer.ellipse(smallerShape, fill=(0, 0, 0, 0))
+
+ # Lilypad
+ elif shape == "lilypad":
+ drawer.pieslice(smallerShape, 290, 250, fill=self.color)
+
+ # Pie
+ elif shape == "pie":
+ drawer.pieslice(outlineShape, 35, 320, fill=self.color)
+
+ hX, hY = scale(50, self.pxWidth, self.pxHeight, int) # halfline
+ tX, tY = scale(33, self.pxWidth, self.pxHeight, int) # thirdline
+ qX, qY = scale(20, self.pxWidth, self.pxHeight, int) # quarterline
+
+ # Path
+ if shape == "path":
+ drawer.ellipse(rect, fill=self.color)
+ rects = {
+ direction: False
+ for direction in (
+ "up",
+ "down",
+ "left",
+ "right",
+ )
+ }
+ for cell in self.nearbyCoords(x, y):
+ if cell not in grid:
+ continue
+ if cell[0] == x:
+ if cell[1] < y:
+ rects["up"] = True
+ if cell[1] > y:
+ rects["down"] = True
+ if cell[1] == y:
+ if cell[0] < x:
+ rects["left"] = True
+ if cell[0] > x:
+ rects["right"] = True
+
+ for direction, rect in rects.items():
+ if rect:
+ if direction == "up":
+ sect = (
+ (drawPtX, drawPtY),
+ (drawPtX + self.pxWidth, drawPtY + hY),
+ )
+ elif direction == "down":
+ sect = (
+ (drawPtX, drawPtY + hY),
+ (
+ drawPtX + self.pxWidth,
+ drawPtY + self.pxHeight,
+ ),
+ )
+ elif direction == "left":
+ sect = (
+ (drawPtX, drawPtY),
+ (drawPtX + hX, drawPtY + self.pxHeight),
+ )
+ elif direction == "right":
+ sect = (
+ (drawPtX + hX, drawPtY),
+ (
+ drawPtX + self.pxWidth,
+ drawPtY + self.pxHeight,
+ ),
+ )
+ drawer.rectangle(sect, fill=self.color)
+
+ # Duck
+ elif shape == "duck":
+ duckHead = (
+ (drawPtX + qX, drawPtY + qY),
+ (drawPtX + int(qX * 3), drawPtY + int(tY * 2)),
+ )
+ duckBeak = (
+ (drawPtX + hX, drawPtY + qY),
+ (drawPtX + self.pxWidth + qX, drawPtY + int(qY * 3)),
+ )
+ duckWing = ((drawPtX, drawPtY + hY), rect[1])
+ duckBody = (
+ (drawPtX + int(qX / 4), drawPtY + int(qY * 3)),
+ (drawPtX + int(tX * 2), drawPtY + self.pxHeight),
+ )
+ drawer.ellipse(duckBody, fill=self.color)
+ drawer.ellipse(duckHead, fill=self.color)
+ drawer.pieslice(duckWing, 130, 200, fill=self.color)
+ drawer.pieslice(duckBeak, 145, 200, fill=self.color)
+
+ # Peace
+ elif shape == "peace":
+ line = (
+ (
+ drawPtX + hX - int(tenthX / 2),
+ drawPtY + int(tenthY / 2),
+ ),
+ (
+ drawPtX + hX + int(tenthX / 2),
+ drawPtY + self.pxHeight - int(tenthY / 2),
+ ),
+ )
+ drawer.ellipse(outlineShape, fill=self.color)
+ drawer.ellipse(smallerShape, fill=(0, 0, 0, 0))
+ drawer.rectangle(line, fill=self.color)
+
+ def slantLine(difference):
+ return (
+ (drawPtX + difference),
+ (drawPtY + self.pxHeight - qY),
+ ), (
+ (drawPtX + hX),
+ (drawPtY + hY),
+ )
+
+ drawer.line(slantLine(qX), fill=self.color, width=tenthX)
+ drawer.line(slantLine(self.pxWidth - qX), fill=self.color, width=tenthX)
+
+ for x, y in grid:
+ drawPtX = x * self.pxWidth
+ if drawPtX > self.width:
+ continue
+ drawPtY = y * self.pxHeight
+ if drawPtY > self.height:
+ continue
+
+ if self.customImg:
+ drawCustomImg()
+ else:
+ drawShape()
+
+ if self.shadow:
+ shadImg = ImageEnhance.Contrast(frame).enhance(0.0)
+ shadImg = shadImg.filter(ImageFilter.GaussianBlur(5.00))
+ shadImg = ImageChops.offset(shadImg, -2, 2)
+ shadImg.paste(frame, box=(0, 0), mask=frame)
+ frame = shadImg
+ if self.showGrid:
+ drawer = ImageDraw.Draw(frame)
+ w, h = scale(0.05, self.width, self.height, int)
+ for x in range(self.pxWidth, self.width, self.pxWidth):
+ drawer.rectangle(
+ ((x, 0), (x + w, self.height)),
+ fill=self.color,
+ )
+ for y in range(self.pxHeight, self.height, self.pxHeight):
+ drawer.rectangle(
+ ((0, y), (self.width, y + h)),
+ fill=self.color,
+ )
+
+ return frame
+
+ def gridForTick(self, tick):
+ """
+ Given a tick number over 0, returns a new grid (a set of tuples).
+ This must compute the previous ticks' grids if not already computed
+ """
+ if tick - 1 not in self.tickGrids:
+ self.tickGrids[tick - 1] = self.gridForTick(tick - 1)
+
+ lastGrid = self.tickGrids[tick - 1]
+
+ def neighbours(x, y):
+ return {cell for cell in self.nearbyCoords(x, y) if cell in lastGrid}
+
+ newGrid = set()
+ # Copy cells from the previous grid if they have 2 or 3 neighbouring cells
+ # and if they are within the grid or its bleed area (off-canvas area)
+ for x, y in lastGrid:
+ if (
+ -self.bleedSize > x > self.gridWidth + self.bleedSize
+ or -self.bleedSize > y > self.gridHeight + self.bleedSize
+ ):
+ continue
+ surrounding = len(neighbours(x, y))
+ if surrounding == 2 or surrounding == 3:
+ newGrid.add((x, y))
+
+ # Find positions around living cells which must be checked for reproduction
+ potentialNewCells = {
+ coordTup
+ for origin in lastGrid
+ for coordTup in list(self.nearbyCoords(*origin))
+ }
+ # Check for reproduction
+ for x, y in potentialNewCells:
+ if (x, y) in newGrid:
+ # Ignore non-empty cell
+ continue
+ surrounding = len(neighbours(x, y))
+ if surrounding == 3:
+ newGrid.add((x, y))
+
+ return newGrid
+
+ def savePreset(self):
+ pr = super().savePreset()
+ pr["GRID"] = sorted(self.startingGrid)
+ return pr
+
+ def loadPreset(self, pr, *args):
+ self.startingGrid = set(pr["GRID"])
+ if self.startingGrid:
+ for widget in self.shiftButtons:
+ widget.setEnabled(True)
+ super().loadPreset(pr, *args)
+
+ def nearbyCoords(self, x, y):
+ yield x + 1, y + 1
+ yield x + 1, y - 1
+ yield x - 1, y + 1
+ yield x - 1, y - 1
+ yield x, y + 1
+ yield x, y - 1
+ yield x + 1, y
+ yield x - 1, y
+
+
+class ClickGrid(QUndoCommand):
+ def __init__(self, comp, pos, button):
+ super().__init__("click %s component #%s" % (comp.name, comp.compPos))
+ self.comp = comp
+ self.pos = [pos]
+ if button == QtCore.Qt.MouseButton.RightButton:
+ self.button = 2
+ else:
+ self.button = 1
+
+ def id(self):
+ return self.button
+
+ def mergeWith(self, other):
+ self.pos.extend(other.pos)
+ return True
+
+ def add(self):
+ for pos in self.pos[:]:
+ self.comp.startingGrid.add(pos)
+ self.comp.update(auto=True)
+
+ def remove(self):
+ for pos in self.pos[:]:
+ self.comp.startingGrid.discard(pos)
+ self.comp.update(auto=True)
+
+ def redo(self):
+ if self.button == 1: # Left-click
+ self.add()
+ elif self.button == 2: # Right-click
+ self.remove()
+
+ def undo(self):
+ if self.button == 1: # Left-click
+ self.remove()
+ elif self.button == 2: # Right-click
+ self.add()
+
+
+class ShiftGrid(QUndoCommand):
+ def __init__(self, comp, direction):
+ super().__init__("change %s component #%s" % (comp.name, comp.compPos))
+ self.comp = comp
+ self.direction = direction
+ self.distance = 1
+
+ def id(self):
+ return self.direction
+
+ def mergeWith(self, other):
+ self.distance += other.distance
+ return True
+
+ def newGrid(self, Xchange, Ychange):
+ return {(x + Xchange, y + Ychange) for x, y in self.comp.startingGrid}
+
+ def redo(self):
+ if self.direction == 0:
+ newGrid = self.newGrid(0, -self.distance)
+ elif self.direction == 1:
+ newGrid = self.newGrid(0, self.distance)
+ elif self.direction == 2:
+ newGrid = self.newGrid(-self.distance, 0)
+ elif self.direction == 3:
+ newGrid = self.newGrid(self.distance, 0)
+ self.comp.startingGrid = newGrid
+ self.comp._sendUpdateSignal()
+
+ def undo(self):
+ if self.direction == 0:
+ newGrid = self.newGrid(0, self.distance)
+ elif self.direction == 1:
+ newGrid = self.newGrid(0, -self.distance)
+ elif self.direction == 2:
+ newGrid = self.newGrid(self.distance, 0)
+ elif self.direction == 3:
+ newGrid = self.newGrid(-self.distance, 0)
+ self.comp.startingGrid = newGrid
+ self.comp._sendUpdateSignal()
diff --git a/src/avp/components/life.ui b/src/avp/components/life.ui
new file mode 100644
index 0000000..30cf9d0
--- /dev/null
+++ b/src/avp/components/life.ui
@@ -0,0 +1,405 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<ui version="4.0">
+ <class>Form</class>
+ <widget class="QWidget" name="Form">
+ <property name="geometry">
+ <rect>
+ <x>0</x>
+ <y>0</y>
+ <width>586</width>
+ <height>197</height>
+ </rect>
+ </property>
+ <property name="windowTitle">
+ <string>Form</string>
+ </property>
+ <layout class="QVBoxLayout" name="verticalLayout_2">
+ <item>
+ <layout class="QHBoxLayout" name="horizontalLayout_3">
+ <item>
+ <layout class="QVBoxLayout" name="verticalLayout">
+ <item>
+ <layout class="QHBoxLayout" name="horizontalLayout_4">
+ <item>
+ <widget class="QLabel" name="label_5">
+ <property name="text">
+ <string>Simulation Speed</string>
+ </property>
+ </widget>
+ </item>
+ <item>
+ <widget class="QSpinBox" name="spinBox_tickRate">
+ <property name="suffix">
+ <string> frames per tick</string>
+ </property>
+ <property name="minimum">
+ <number>1</number>
+ </property>
+ <property name="maximum">
+ <number>30</number>
+ </property>
+ <property name="value">
+ <number>5</number>
+ </property>
+ </widget>
+ </item>
+ <item>
+ <spacer name="horizontalSpacer">
+ <property name="orientation">
+ <enum>Qt::Horizontal</enum>
+ </property>
+ <property name="sizeHint" stdset="0">
+ <size>
+ <width>40</width>
+ <height>20</height>
+ </size>
+ </property>
+ </spacer>
+ </item>
+ <item>
+ <widget class="QLineEdit" name="lineEdit_color">
+ <property name="maximumSize">
+ <size>
+ <width>0</width>
+ <height>16777215</height>
+ </size>
+ </property>
+ <property name="text">
+ <string>255,255,255</string>
+ </property>
+ </widget>
+ </item>
+ </layout>
+ </item>
+ <item>
+ <layout class="QHBoxLayout" name="horizontalLayout_5">
+ <item>
+ <widget class="QLabel" name="label_7">
+ <property name="text">
+ <string>Grid Scale</string>
+ </property>
+ </widget>
+ </item>
+ <item>
+ <widget class="QSpinBox" name="spinBox_scale">
+ <property name="minimum">
+ <number>22</number>
+ </property>
+ <property name="maximum">
+ <number>128</number>
+ </property>
+ <property name="value">
+ <number>32</number>
+ </property>
+ </widget>
+ </item>
+ <item>
+ <widget class="QCheckBox" name="checkBox_customImg">
+ <property name="text">
+ <string>Custom Image</string>
+ </property>
+ </widget>
+ </item>
+ <item>
+ <spacer name="horizontalSpacer_5">
+ <property name="orientation">
+ <enum>Qt::Horizontal</enum>
+ </property>
+ <property name="sizeHint" stdset="0">
+ <size>
+ <width>40</width>
+ <height>20</height>
+ </size>
+ </property>
+ </spacer>
+ </item>
+ </layout>
+ </item>
+ <item>
+ <layout class="QHBoxLayout" name="horizontalLayout_8">
+ <item>
+ <widget class="QLabel" name="label_image">
+ <property name="text">
+ <string>Image</string>
+ </property>
+ </widget>
+ </item>
+ <item>
+ <widget class="QLineEdit" name="lineEdit_image"/>
+ </item>
+ <item>
+ <widget class="QPushButton" name="pushButton_pickImage">
+ <property name="sizePolicy">
+ <sizepolicy hsizetype="Minimum" vsizetype="Fixed">
+ <horstretch>0</horstretch>
+ <verstretch>0</verstretch>
+ </sizepolicy>
+ </property>
+ <property name="maximumSize">
+ <size>
+ <width>32</width>
+ <height>32</height>
+ </size>
+ </property>
+ <property name="text">
+ <string>...</string>
+ </property>
+ </widget>
+ </item>
+ <item>
+ <widget class="QLabel" name="label_color">
+ <property name="text">
+ <string>Color</string>
+ </property>
+ </widget>
+ </item>
+ <item>
+ <widget class="QLineEdit" name="lineEdit_color_3">
+ <property name="maximumSize">
+ <size>
+ <width>0</width>
+ <height>16777215</height>
+ </size>
+ </property>
+ <property name="text">
+ <string>0,0,0</string>
+ </property>
+ </widget>
+ </item>
+ <item>
+ <widget class="QPushButton" name="pushButton_color">
+ <property name="sizePolicy">
+ <sizepolicy hsizetype="Fixed" vsizetype="Fixed">
+ <horstretch>0</horstretch>
+ <verstretch>0</verstretch>
+ </sizepolicy>
+ </property>
+ <property name="maximumSize">
+ <size>
+ <width>32</width>
+ <height>32</height>
+ </size>
+ </property>
+ <property name="text">
+ <string/>
+ </property>
+ <property name="default">
+ <bool>false</bool>
+ </property>
+ <property name="flat">
+ <bool>false</bool>
+ </property>
+ </widget>
+ </item>
+ <item>
+ <spacer name="horizontalSpacer_2">
+ <property name="orientation">
+ <enum>Qt::Horizontal</enum>
+ </property>
+ <property name="sizeHint" stdset="0">
+ <size>
+ <width>40</width>
+ <height>20</height>
+ </size>
+ </property>
+ </spacer>
+ </item>
+ <item>
+ <widget class="QLabel" name="label_shape">
+ <property name="text">
+ <string>Shape</string>
+ </property>
+ </widget>
+ </item>
+ <item>
+ <widget class="QComboBox" name="comboBox_shapeType">
+ <item>
+ <property name="text">
+ <string>Path</string>
+ </property>
+ </item>
+ <item>
+ <property name="text">
+ <string>Rectangle</string>
+ </property>
+ </item>
+ <item>
+ <property name="text">
+ <string>Elliptical</string>
+ </property>
+ </item>
+ <item>
+ <property name="text">
+ <string>Circle</string>
+ </property>
+ </item>
+ <item>
+ <property name="text">
+ <string>Lilypad</string>
+ </property>
+ </item>
+ <item>
+ <property name="text">
+ <string>Pie</string>
+ </property>
+ </item>
+ <item>
+ <property name="text">
+ <string>Duck</string>
+ </property>
+ </item>
+ <item>
+ <property name="text">
+ <string>Peace</string>
+ </property>
+ </item>
+ </widget>
+ </item>
+ <item>
+ <spacer name="horizontalSpacer_8">
+ <property name="orientation">
+ <enum>Qt::Horizontal</enum>
+ </property>
+ <property name="sizeHint" stdset="0">
+ <size>
+ <width>40</width>
+ <height>20</height>
+ </size>
+ </property>
+ </spacer>
+ </item>
+ </layout>
+ </item>
+ <item>
+ <layout class="QHBoxLayout" name="horizontalLayout_6">
+ <item>
+ <widget class="QCheckBox" name="checkBox_shadow">
+ <property name="text">
+ <string>Shadow</string>
+ </property>
+ </widget>
+ </item>
+ <item>
+ <widget class="QCheckBox" name="checkBox_showGrid">
+ <property name="text">
+ <string>Show Grid</string>
+ </property>
+ </widget>
+ </item>
+ <item>
+ <spacer name="horizontalSpacer_6">
+ <property name="orientation">
+ <enum>Qt::Horizontal</enum>
+ </property>
+ <property name="sizeHint" stdset="0">
+ <size>
+ <width>40</width>
+ <height>20</height>
+ </size>
+ </property>
+ </spacer>
+ </item>
+ </layout>
+ </item>
+ <item>
+ <layout class="QHBoxLayout" name="horizontalLayout_9">
+ <item>
+ <widget class="QToolButton" name="toolButton_up">
+ <property name="text">
+ <string>Up</string>
+ </property>
+ <property name="arrowType">
+ <enum>Qt::UpArrow</enum>
+ </property>
+ </widget>
+ </item>
+ <item>
+ <widget class="QToolButton" name="toolButton_down">
+ <property name="text">
+ <string>Down</string>
+ </property>
+ <property name="arrowType">
+ <enum>Qt::DownArrow</enum>
+ </property>
+ </widget>
+ </item>
+ <item>
+ <widget class="QToolButton" name="toolButton_left">
+ <property name="text">
+ <string>Left</string>
+ </property>
+ <property name="arrowType">
+ <enum>Qt::LeftArrow</enum>
+ </property>
+ </widget>
+ </item>
+ <item>
+ <widget class="QToolButton" name="toolButton_right">
+ <property name="text">
+ <string>Right</string>
+ </property>
+ <property name="arrowType">
+ <enum>Qt::RightArrow</enum>
+ </property>
+ </widget>
+ </item>
+ <item>
+ <spacer name="horizontalSpacer_9">
+ <property name="orientation">
+ <enum>Qt::Horizontal</enum>
+ </property>
+ <property name="sizeHint" stdset="0">
+ <size>
+ <width>40</width>
+ <height>20</height>
+ </size>
+ </property>
+ </spacer>
+ </item>
+ </layout>
+ </item>
+ </layout>
+ </item>
+ <item>
+ <widget class="QTextBrowser" name="textBrowser">
+ <property name="html">
+ <string>&lt;!DOCTYPE HTML PUBLIC &quot;-//W3C//DTD HTML 4.0//EN&quot; &quot;http://www.w3.org/TR/REC-html40/strict.dtd&quot;&gt;
+&lt;html&gt;&lt;head&gt;&lt;meta name=&quot;qrichtext&quot; content=&quot;1&quot; /&gt;&lt;style type=&quot;text/css&quot;&gt;
+p, li { white-space: pre-wrap; }
+&lt;/style&gt;&lt;/head&gt;&lt;body style=&quot; font-family:'Ubuntu'; font-size:11pt; font-weight:400; font-style:normal;&quot;&gt;
+&lt;p style=&quot; margin-top:12px; margin-bottom:12px; margin-left:0px; margin-right:0px; -qt-block-indent:0; text-indent:0px;&quot;&gt;&lt;span style=&quot; font-weight:600;&quot;&gt;Click the preview window to place a cell. Right-click to remove.&lt;/span&gt;&lt;/p&gt;
+&lt;p style=&quot; margin-top:12px; margin-bottom:12px; margin-left:0px; margin-right:0px; -qt-block-indent:0; text-indent:0px;&quot;&gt;- A cell with less than 2 neighbours will die from underpopulation&lt;/p&gt;
+&lt;p style=&quot; margin-top:12px; margin-bottom:12px; margin-left:0px; margin-right:0px; -qt-block-indent:0; text-indent:0px;&quot;&gt;- A cell with more than 3 neighbours will die from overpopulation.&lt;/p&gt;
+&lt;p style=&quot; margin-top:12px; margin-bottom:12px; margin-left:0px; margin-right:0px; -qt-block-indent:0; text-indent:0px;&quot;&gt;- An empty space surrounded by 3 live cells will cause reproduction.&lt;/p&gt;&lt;/body&gt;&lt;/html&gt;</string>
+ </property>
+ <property name="tabStopDistance">
+ <number>80</number>
+ </property>
+ <property name="textInteractionFlags">
+ <set>Qt::NoTextInteraction</set>
+ </property>
+ <property name="openLinks">
+ <bool>false</bool>
+ </property>
+ </widget>
+ </item>
+ </layout>
+ </item>
+ <item>
+ <spacer name="verticalSpacer">
+ <property name="orientation">
+ <enum>Qt::Vertical</enum>
+ </property>
+ <property name="sizeHint" stdset="0">
+ <size>
+ <width>20</width>
+ <height>40</height>
+ </size>
+ </property>
+ </spacer>
+ </item>
+ </layout>
+ </widget>
+ <resources/>
+ <connections/>
+</ui>
diff --git a/src/avp/components/original.py b/src/avp/components/original.py
new file mode 100644
index 0000000..fad797b
--- /dev/null
+++ b/src/avp/components/original.py
@@ -0,0 +1,243 @@
+import numpy
+from PIL import Image, ImageDraw
+from copy import copy
+
+from ..component import Component
+from ..toolkit.frame import BlankFrame
+
+
+class Component(Component):
+ name = "Classic Visualizer"
+ version = "1.0.0"
+
+ def names(*args):
+ return ["Original Audio Visualization"]
+
+ def properties(self):
+ return ["pcm"]
+
+ def widget(self, *args):
+ self.scale = 20
+ self.y = 0
+ super().widget(*args)
+
+ self.page.comboBox_visLayout.addItem("Classic")
+ self.page.comboBox_visLayout.addItem("Split")
+ self.page.comboBox_visLayout.addItem("Bottom")
+ self.page.comboBox_visLayout.addItem("Top")
+ self.page.comboBox_visLayout.setCurrentIndex(0)
+
+ self.page.lineEdit_visColor.setText("255,255,255")
+
+ self.trackWidgets(
+ {
+ "visColor": self.page.lineEdit_visColor,
+ "layout": self.page.comboBox_visLayout,
+ "scale": self.page.spinBox_scale,
+ "y": self.page.spinBox_y,
+ "smooth": self.page.spinBox_smooth,
+ },
+ colorWidgets={
+ "visColor": self.page.pushButton_visColor,
+ },
+ relativeWidgets=[
+ "y",
+ ],
+ )
+
+ def previewRender(self):
+ spectrum = numpy.fromfunction(
+ lambda x: float(self.scale) / 2500 * (x - 128) ** 2,
+ (255,),
+ dtype="int16",
+ )
+ return self.drawBars(
+ self.width, self.height, spectrum, self.visColor, self.layout
+ )
+
+ def preFrameRender(self, **kwargs):
+ super().preFrameRender(**kwargs)
+ self.smoothConstantDown = 0.08 + 0 if not self.smooth else self.smooth / 15
+ self.smoothConstantUp = 0.8 - 0 if not self.smooth else self.smooth / 15
+ self.lastSpectrum = None
+ self.spectrumArray = {}
+
+ for i in range(0, len(self.completeAudioArray), self.sampleSize):
+ if self.canceled:
+ break
+ self.lastSpectrum = self.transformData(
+ i,
+ self.completeAudioArray,
+ self.sampleSize,
+ self.smoothConstantDown,
+ self.smoothConstantUp,
+ self.lastSpectrum,
+ )
+ self.spectrumArray[i] = copy(self.lastSpectrum)
+
+ progress = int(100 * (i / len(self.completeAudioArray)))
+ if progress >= 100:
+ progress = 100
+ pStr = "Analyzing audio: " + str(progress) + "%"
+ self.progressBarSetText.emit(pStr)
+ self.progressBarUpdate.emit(int(progress))
+
+ def frameRender(self, frameNo):
+ arrayNo = frameNo * self.sampleSize
+ return self.drawBars(
+ self.width,
+ self.height,
+ self.spectrumArray[arrayNo],
+ self.visColor,
+ self.layout,
+ )
+
+ def transformData(
+ self,
+ i,
+ completeAudioArray,
+ sampleSize,
+ smoothConstantDown,
+ smoothConstantUp,
+ lastSpectrum,
+ ):
+ if len(completeAudioArray) < (i + sampleSize):
+ sampleSize = len(completeAudioArray) - i
+
+ window = numpy.hanning(sampleSize)
+ data = completeAudioArray[i : i + sampleSize][::1] * window
+ paddedSampleSize = 2048
+ paddedData = numpy.pad(data, (0, paddedSampleSize - sampleSize), "constant")
+ spectrum = numpy.fft.fft(paddedData)
+ sample_rate = 44100
+ frequencies = numpy.fft.fftfreq(len(spectrum), 1.0 / sample_rate)
+
+ y = abs(spectrum[0 : int(paddedSampleSize / 2) - 1])
+
+ # filter the noise away
+ # y[y<80] = 0
+
+ y = self.scale * numpy.log10(y)
+ y[numpy.isinf(y)] = 0
+
+ if lastSpectrum is not None:
+ lastSpectrum[y < lastSpectrum] = y[
+ y < lastSpectrum
+ ] * smoothConstantDown + lastSpectrum[y < lastSpectrum] * (
+ 1 - smoothConstantDown
+ )
+
+ lastSpectrum[y >= lastSpectrum] = y[
+ y >= lastSpectrum
+ ] * smoothConstantUp + lastSpectrum[y >= lastSpectrum] * (
+ 1 - smoothConstantUp
+ )
+ else:
+ lastSpectrum = y
+
+ x = frequencies[0 : int(paddedSampleSize / 2) - 1]
+
+ return lastSpectrum
+
+ def drawBars(self, width, height, spectrum, color, layout):
+ vH = height - height / 8
+ bF = width / 64
+ bH = bF / 2
+ bQ = bF / 4
+ imTop = BlankFrame(width, height)
+ draw = ImageDraw.Draw(imTop)
+ r, g, b = color
+ color2 = (r, g, b, 125)
+
+ bP = height / 1200
+
+ for j in range(0, 63):
+ x0 = bH + j * bF
+ y0 = vH + bQ
+ y1 = vH + bQ - spectrum[j * 4] * bP - bH
+ x1 = bH + j * bF + bF
+ draw.rectangle(
+ (
+ x0,
+ y0 if y0 < y1 else y1,
+ x1 if x1 > x0 else x0,
+ y1 if y0 < y1 else y0,
+ ),
+ fill=color2,
+ )
+
+ x0 = bH + bQ + j * bF
+ y0 = vH
+ x1 = bH + bQ + j * bF + bH
+ y1 = vH - spectrum[j * 4] * bP
+ draw.rectangle(
+ (
+ x0,
+ y0 if y0 < y1 else y1,
+ x1 if x1 > x0 else x0,
+ y1 if y0 < y1 else y0,
+ ),
+ fill=color,
+ )
+
+ imBottom = imTop.transpose(Image.Transpose.FLIP_TOP_BOTTOM)
+
+ im = BlankFrame(width, height)
+
+ if layout == 0: # Classic
+ y = self.y - int(height / 100 * 43)
+ im.paste(imTop, (0, y), mask=imTop)
+ y = self.y + int(height / 100 * 43)
+ im.paste(imBottom, (0, y), mask=imBottom)
+
+ if layout == 1: # Split
+ y = self.y + int(height / 100 * 10)
+ im.paste(imTop, (0, y), mask=imTop)
+ y = self.y - int(height / 100 * 10)
+ im.paste(imBottom, (0, y), mask=imBottom)
+
+ if layout == 2: # Bottom
+ y = self.y + int(height / 100 * 10)
+ im.paste(imTop, (0, y), mask=imTop)
+
+ if layout == 3: # Top
+ y = self.y - int(height / 100 * 10)
+ im.paste(imBottom, (0, y), mask=imBottom)
+
+ return im
+
+ def command(self, arg):
+ if "=" in arg:
+ key, arg = arg.split("=", 1)
+ try:
+ if key == "color":
+ self.page.lineEdit_visColor.setText(arg)
+ return
+ elif key == "layout":
+ if arg == "classic":
+ self.page.comboBox_visLayout.setCurrentIndex(0)
+ elif arg == "split":
+ self.page.comboBox_visLayout.setCurrentIndex(1)
+ elif arg == "bottom":
+ self.page.comboBox_visLayout.setCurrentIndex(2)
+ elif arg == "top":
+ self.page.comboBox_visLayout.setCurrentIndex(3)
+ return
+ elif key == "scale":
+ arg = int(arg)
+ self.page.spinBox_scale.setValue(arg)
+ return
+ elif key == "y":
+ arg = int(arg)
+ self.page.spinBox_y.setValue(arg)
+ return
+ except ValueError:
+ print("You must enter a number.")
+ quit(1)
+ super().command(arg)
+
+ def commandHelp(self):
+ print("Give a layout name:\n layout=[classic/split/bottom/top]")
+ print("Specify a color:\n color=255,255,255")
+ print("Visualizer scale (20 is default):\n scale=number")
+ print("Y position:\n y=number")
diff --git a/src/avp/components/original.ui b/src/avp/components/original.ui
new file mode 100644
index 0000000..c7b7e22
--- /dev/null
+++ b/src/avp/components/original.ui
@@ -0,0 +1,243 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<ui version="4.0">
+ <class>Form</class>
+ <widget class="QWidget" name="Form">
+ <property name="geometry">
+ <rect>
+ <x>0</x>
+ <y>0</y>
+ <width>586</width>
+ <height>178</height>
+ </rect>
+ </property>
+ <property name="minimumSize">
+ <size>
+ <width>180</width>
+ <height>0</height>
+ </size>
+ </property>
+ <property name="windowTitle">
+ <string>Form</string>
+ </property>
+ <layout class="QVBoxLayout" name="verticalLayout">
+ <item>
+ <layout class="QHBoxLayout" name="horizontalLayout_9">
+ <property name="leftMargin">
+ <number>4</number>
+ </property>
+ <item>
+ <widget class="QLabel" name="label_visLayout">
+ <property name="sizePolicy">
+ <sizepolicy hsizetype="Fixed" vsizetype="Preferred">
+ <horstretch>0</horstretch>
+ <verstretch>0</verstretch>
+ </sizepolicy>
+ </property>
+ <property name="text">
+ <string>Layout</string>
+ </property>
+ </widget>
+ </item>
+ <item>
+ <widget class="QComboBox" name="comboBox_visLayout"/>
+ </item>
+ <item>
+ <spacer name="horizontalSpacer_5">
+ <property name="orientation">
+ <enum>Qt::Horizontal</enum>
+ </property>
+ <property name="sizeType">
+ <enum>QSizePolicy::Fixed</enum>
+ </property>
+ <property name="sizeHint" stdset="0">
+ <size>
+ <width>5</width>
+ <height>20</height>
+ </size>
+ </property>
+ </spacer>
+ </item>
+ <item>
+ <widget class="QLabel" name="label_visColor">
+ <property name="text">
+ <string>Color</string>
+ </property>
+ </widget>
+ </item>
+ <item>
+ <widget class="QPushButton" name="pushButton_visColor">
+ <property name="maximumSize">
+ <size>
+ <width>32</width>
+ <height>32</height>
+ </size>
+ </property>
+ <property name="text">
+ <string/>
+ </property>
+ <property name="MaximumSize" stdset="0">
+ <size>
+ <width>32</width>
+ <height>32</height>
+ </size>
+ </property>
+ </widget>
+ </item>
+ <item>
+ <widget class="QLineEdit" name="lineEdit_visColor"/>
+ </item>
+ <item>
+ <spacer name="horizontalSpacer_4">
+ <property name="orientation">
+ <enum>Qt::Horizontal</enum>
+ </property>
+ <property name="sizeType">
+ <enum>QSizePolicy::Fixed</enum>
+ </property>
+ <property name="sizeHint" stdset="0">
+ <size>
+ <width>5</width>
+ <height>20</height>
+ </size>
+ </property>
+ </spacer>
+ </item>
+ <item>
+ <widget class="QLabel" name="label_Y">
+ <property name="text">
+ <string>Y</string>
+ </property>
+ </widget>
+ </item>
+ <item>
+ <widget class="QSpinBox" name="spinBox_y">
+ <property name="buttonSymbols">
+ <enum>QAbstractSpinBox::UpDownArrows</enum>
+ </property>
+ <property name="minimum">
+ <number>-5000</number>
+ </property>
+ <property name="maximum">
+ <number>5000</number>
+ </property>
+ <property name="singleStep">
+ <number>10</number>
+ </property>
+ <property name="value">
+ <number>0</number>
+ </property>
+ </widget>
+ </item>
+ <item>
+ <spacer name="horizontalSpacer_2">
+ <property name="orientation">
+ <enum>Qt::Horizontal</enum>
+ </property>
+ <property name="sizeHint" stdset="0">
+ <size>
+ <width>40</width>
+ <height>20</height>
+ </size>
+ </property>
+ </spacer>
+ </item>
+ </layout>
+ </item>
+ <item>
+ <layout class="QHBoxLayout" name="horizontalLayout_10">
+ <property name="leftMargin">
+ <number>4</number>
+ </property>
+ <item>
+ <widget class="QLabel" name="label_scale">
+ <property name="text">
+ <string>Scale</string>
+ </property>
+ </widget>
+ </item>
+ <item>
+ <widget class="QSpinBox" name="spinBox_scale">
+ <property name="buttonSymbols">
+ <enum>QAbstractSpinBox::PlusMinus</enum>
+ </property>
+ <property name="minimum">
+ <number>1</number>
+ </property>
+ <property name="value">
+ <number>20</number>
+ </property>
+ </widget>
+ </item>
+ <item>
+ <spacer name="horizontalSpacer_3">
+ <property name="orientation">
+ <enum>Qt::Horizontal</enum>
+ </property>
+ <property name="sizeType">
+ <enum>QSizePolicy::Expanding</enum>
+ </property>
+ <property name="sizeHint" stdset="0">
+ <size>
+ <width>40</width>
+ <height>20</height>
+ </size>
+ </property>
+ </spacer>
+ </item>
+ </layout>
+ </item>
+ <item>
+ <layout class="QHBoxLayout" name="horizontalLayout">
+ <property name="sizeConstraint">
+ <enum>QLayout::SetDefaultConstraint</enum>
+ </property>
+ <property name="leftMargin">
+ <number>4</number>
+ </property>
+ <item>
+ <widget class="QLabel" name="label_smooth">
+ <property name="text">
+ <string>Sensitivity</string>
+ </property>
+ </widget>
+ </item>
+ <item>
+ <widget class="QSpinBox" name="spinBox_smooth">
+ <property name="maximum">
+ <number>5</number>
+ </property>
+ </widget>
+ </item>
+ <item>
+ <spacer name="horizontalSpacer">
+ <property name="orientation">
+ <enum>Qt::Horizontal</enum>
+ </property>
+ <property name="sizeHint" stdset="0">
+ <size>
+ <width>40</width>
+ <height>20</height>
+ </size>
+ </property>
+ </spacer>
+ </item>
+ </layout>
+ </item>
+ <item>
+ <spacer name="verticalSpacer">
+ <property name="orientation">
+ <enum>Qt::Vertical</enum>
+ </property>
+ <property name="sizeHint" stdset="0">
+ <size>
+ <width>20</width>
+ <height>40</height>
+ </size>
+ </property>
+ </spacer>
+ </item>
+ </layout>
+ </widget>
+ <resources/>
+ <connections/>
+</ui>
diff --git a/src/avp/components/sound.py b/src/avp/components/sound.py
new file mode 100644
index 0000000..2df8e38
--- /dev/null
+++ b/src/avp/components/sound.py
@@ -0,0 +1,77 @@
+from PyQt6 import QtGui, QtCore, QtWidgets
+import os
+
+from ..component import Component
+from ..toolkit.frame import BlankFrame
+
+
+class Component(Component):
+ name = "Sound"
+ version = "1.0.0"
+
+ def widget(self, *args):
+ super().widget(*args)
+ self.page.pushButton_sound.clicked.connect(self.pickSound)
+ self.trackWidgets(
+ {
+ "sound": self.page.lineEdit_sound,
+ "chorus": self.page.checkBox_chorus,
+ "delay": self.page.spinBox_delay,
+ "volume": self.page.spinBox_volume,
+ },
+ commandArgs={
+ "sound": None,
+ },
+ )
+
+ def properties(self):
+ props = ["static", "audio"]
+ if not os.path.exists(self.sound):
+ props.append("error")
+ return props
+
+ def error(self):
+ if not self.sound:
+ return "No audio file selected."
+ if not os.path.exists(self.sound):
+ return "The audio file selected no longer exists!"
+
+ def audio(self):
+ params = {}
+ if self.delay != 0.0:
+ params["adelay"] = "=%s" % str(int(self.delay * 1000.00))
+ if self.chorus:
+ params["chorus"] = "=0.5:0.9:50|60|40:0.4|0.32|0.3:0.25|0.4|0.3:2|2.3|1.3"
+ if self.volume != 1.0:
+ params["volume"] = "=%s:replaygain_noclip=0" % str(self.volume)
+
+ return (self.sound, params)
+
+ def pickSound(self):
+ sndDir = self.settings.value("componentDir", os.path.expanduser("~"))
+ filename, _ = QtWidgets.QFileDialog.getOpenFileName(
+ self.page,
+ "Choose Sound",
+ sndDir,
+ "Audio Files (%s)" % " ".join(self.core.audioFormats),
+ )
+ if filename:
+ self.settings.setValue("componentDir", os.path.dirname(filename))
+ self.mergeUndo = False
+ self.page.lineEdit_sound.setText(filename)
+ self.mergeUndo = True
+
+ def commandHelp(self):
+ print("Path to audio file:\n path=/filepath/to/sound.ogg")
+
+ def command(self, arg):
+ if "=" in arg:
+ key, arg = arg.split("=", 1)
+ if key == "path":
+ if "*%s" % os.path.splitext(arg)[1] not in self.core.audioFormats:
+ print("Not a supported audio format")
+ quit(1)
+ self.page.lineEdit_sound.setText(arg)
+ return
+
+ super().command(arg)
diff --git a/src/avp/components/sound.ui b/src/avp/components/sound.ui
new file mode 100644
index 0000000..4c11332
--- /dev/null
+++ b/src/avp/components/sound.ui
@@ -0,0 +1,172 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<ui version="4.0">
+ <class>Form</class>
+ <widget class="QWidget" name="Form">
+ <property name="geometry">
+ <rect>
+ <x>0</x>
+ <y>0</y>
+ <width>586</width>
+ <height>197</height>
+ </rect>
+ </property>
+ <property name="windowTitle">
+ <string>Form</string>
+ </property>
+ <layout class="QVBoxLayout" name="verticalLayout_2">
+ <item>
+ <layout class="QVBoxLayout" name="verticalLayout">
+ <property name="leftMargin">
+ <number>4</number>
+ </property>
+ <item>
+ <layout class="QHBoxLayout" name="horizontalLayout_8">
+ <item>
+ <widget class="QLabel" name="label_textColor">
+ <property name="sizePolicy">
+ <sizepolicy hsizetype="Fixed" vsizetype="Preferred">
+ <horstretch>0</horstretch>
+ <verstretch>0</verstretch>
+ </sizepolicy>
+ </property>
+ <property name="minimumSize">
+ <size>
+ <width>31</width>
+ <height>0</height>
+ </size>
+ </property>
+ <property name="text">
+ <string>Audio File</string>
+ </property>
+ </widget>
+ </item>
+ <item>
+ <widget class="QLineEdit" name="lineEdit_sound">
+ <property name="minimumSize">
+ <size>
+ <width>1</width>
+ <height>0</height>
+ </size>
+ </property>
+ </widget>
+ </item>
+ <item>
+ <widget class="QPushButton" name="pushButton_sound">
+ <property name="sizePolicy">
+ <sizepolicy hsizetype="Fixed" vsizetype="Fixed">
+ <horstretch>0</horstretch>
+ <verstretch>0</verstretch>
+ </sizepolicy>
+ </property>
+ <property name="minimumSize">
+ <size>
+ <width>1</width>
+ <height>0</height>
+ </size>
+ </property>
+ <property name="maximumSize">
+ <size>
+ <width>32</width>
+ <height>32</height>
+ </size>
+ </property>
+ <property name="text">
+ <string>...</string>
+ </property>
+ <property name="MaximumSize" stdset="0">
+ <size>
+ <width>32</width>
+ <height>32</height>
+ </size>
+ </property>
+ </widget>
+ </item>
+ </layout>
+ </item>
+ </layout>
+ </item>
+ <item>
+ <layout class="QHBoxLayout" name="horizontalLayout_2">
+ <item>
+ <widget class="QLabel" name="label_2">
+ <property name="text">
+ <string>Volume</string>
+ </property>
+ </widget>
+ </item>
+ <item>
+ <widget class="QDoubleSpinBox" name="spinBox_volume">
+ <property name="suffix">
+ <string>x</string>
+ </property>
+ <property name="maximum">
+ <double>10.000000000000000</double>
+ </property>
+ <property name="singleStep">
+ <double>0.100000000000000</double>
+ </property>
+ <property name="value">
+ <double>1.000000000000000</double>
+ </property>
+ </widget>
+ </item>
+ <item>
+ <spacer name="horizontalSpacer_2">
+ <property name="orientation">
+ <enum>Qt::Horizontal</enum>
+ </property>
+ <property name="sizeHint" stdset="0">
+ <size>
+ <width>40</width>
+ <height>20</height>
+ </size>
+ </property>
+ </spacer>
+ </item>
+ <item>
+ <widget class="QLabel" name="label">
+ <property name="text">
+ <string>Delay</string>
+ </property>
+ </widget>
+ </item>
+ <item>
+ <widget class="QDoubleSpinBox" name="spinBox_delay">
+ <property name="suffix">
+ <string>s</string>
+ </property>
+ <property name="maximum">
+ <double>9999999.990000000223517</double>
+ </property>
+ <property name="singleStep">
+ <double>0.500000000000000</double>
+ </property>
+ </widget>
+ </item>
+ <item>
+ <widget class="QCheckBox" name="checkBox_chorus">
+ <property name="text">
+ <string>Chorus</string>
+ </property>
+ </widget>
+ </item>
+ </layout>
+ </item>
+ <item>
+ <spacer name="verticalSpacer">
+ <property name="orientation">
+ <enum>Qt::Vertical</enum>
+ </property>
+ <property name="sizeHint" stdset="0">
+ <size>
+ <width>20</width>
+ <height>40</height>
+ </size>
+ </property>
+ </spacer>
+ </item>
+ </layout>
+ </widget>
+ <resources/>
+ <connections/>
+</ui>
diff --git a/src/avp/components/spectrum.py b/src/avp/components/spectrum.py
new file mode 100644
index 0000000..062ebc7
--- /dev/null
+++ b/src/avp/components/spectrum.py
@@ -0,0 +1,368 @@
+from PIL import Image
+from PyQt6 import QtGui, QtCore, QtWidgets
+import os
+import math
+import subprocess
+import time
+import logging
+
+from ..component import Component
+from ..toolkit.frame import BlankFrame, scale
+from ..toolkit import checkOutput, connectWidget
+from ..toolkit.ffmpeg import (
+ openPipe,
+ closePipe,
+ getAudioDuration,
+ FfmpegVideo,
+ exampleSound,
+)
+
+
+log = logging.getLogger("AVP.Components.Spectrum")
+
+
+class Component(Component):
+ name = "Spectrum"
+ version = "1.0.1"
+
+ def widget(self, *args):
+ self.previewFrame = None
+ super().widget(*args)
+ self._image = BlankFrame(self.width, self.height)
+ self.chunkSize = 4 * self.width * self.height
+ self.changedOptions = True
+ self.previewSize = (214, 120)
+ self.previewPipe = None
+
+ if hasattr(self.parent, "lineEdit_audioFile"):
+ # update preview when audio file changes (if genericPreview is off)
+ self.parent.lineEdit_audioFile.textChanged.connect(self.update)
+
+ self.trackWidgets(
+ {
+ "filterType": self.page.comboBox_filterType,
+ "window": self.page.comboBox_window,
+ "mode": self.page.comboBox_mode,
+ "amplitude": self.page.comboBox_amplitude0,
+ "amplitude1": self.page.comboBox_amplitude1,
+ "amplitude2": self.page.comboBox_amplitude2,
+ "display": self.page.comboBox_display,
+ "zoom": self.page.spinBox_zoom,
+ "tc": self.page.spinBox_tc,
+ "x": self.page.spinBox_x,
+ "y": self.page.spinBox_y,
+ "mirror": self.page.checkBox_mirror,
+ "draw": self.page.checkBox_draw,
+ "scale": self.page.spinBox_scale,
+ "color": self.page.comboBox_color,
+ "compress": self.page.checkBox_compress,
+ "mono": self.page.checkBox_mono,
+ "hue": self.page.spinBox_hue,
+ },
+ relativeWidgets=[
+ "x",
+ "y",
+ ],
+ )
+ for widget in self._trackedWidgets.values():
+ connectWidget(widget, lambda: self.changed())
+
+ def changed(self):
+ self.changedOptions = True
+
+ def update(self):
+ filterType = self.page.comboBox_filterType.currentIndex()
+ self.page.stackedWidget.setCurrentIndex(filterType)
+ if filterType == 3:
+ self.page.spinBox_hue.setEnabled(False)
+ else:
+ self.page.spinBox_hue.setEnabled(True)
+ if filterType == 2 or filterType == 4:
+ self.page.checkBox_mono.setEnabled(False)
+ else:
+ self.page.checkBox_mono.setEnabled(True)
+
+ def previewRender(self):
+ changedSize = self.updateChunksize()
+ if (
+ not changedSize
+ and not self.changedOptions
+ and self.previewFrame is not None
+ ):
+ log.debug("Spectrum #%s is reusing old preview frame" % self.compPos)
+ return self.previewFrame
+
+ frame = self.getPreviewFrame()
+ self.changedOptions = False
+ if not frame:
+ log.warning("Spectrum #%s failed to create a preview frame" % self.compPos)
+ self.previewFrame = None
+ return BlankFrame(self.width, self.height)
+ else:
+ self.previewFrame = frame
+ return frame
+
+ def preFrameRender(self, **kwargs):
+ super().preFrameRender(**kwargs)
+ if self.previewPipe is not None:
+ self.previewPipe.wait()
+ self.updateChunksize()
+ w, h = scale(self.scale, self.width, self.height, str)
+ self.video = FfmpegVideo(
+ inputPath=self.audioFile,
+ filter_=self.makeFfmpegFilter(),
+ width=w,
+ height=h,
+ chunkSize=self.chunkSize,
+ frameRate=int(self.settings.value("outputFrameRate")),
+ parent=self.parent,
+ component=self,
+ )
+
+ def frameRender(self, frameNo):
+ if FfmpegVideo.threadError is not None:
+ raise FfmpegVideo.threadError
+ return self.finalizeFrame(self.video.frame(frameNo))
+
+ def postFrameRender(self):
+ closePipe(self.video.pipe)
+
+ def getPreviewFrame(self):
+ genericPreview = self.settings.value("pref_genericPreview")
+ startPt = 0
+ if not genericPreview:
+ inputFile = self.parent.lineEdit_audioFile.text()
+ if not inputFile or not os.path.exists(inputFile):
+ return
+ duration = getAudioDuration(inputFile)
+ if not duration:
+ return
+ startPt = duration / 3
+
+ command = [
+ self.core.FFMPEG_BIN,
+ "-thread_queue_size",
+ "512",
+ "-r",
+ str(self.settings.value("outputFrameRate")),
+ "-ss",
+ "{0:.3f}".format(startPt),
+ "-i",
+ self.core.junkStream if genericPreview else inputFile,
+ "-f",
+ "image2pipe",
+ "-pix_fmt",
+ "rgba",
+ ]
+ command.extend(self.makeFfmpegFilter(preview=True, startPt=startPt))
+ command.extend(
+ [
+ "-an",
+ "-s:v",
+ "%sx%s" % scale(self.scale, self.width, self.height, str),
+ "-codec:v",
+ "rawvideo",
+ "-",
+ "-frames:v",
+ "1",
+ ]
+ )
+
+ if self.core.logEnabled:
+ logFilename = os.path.join(
+ self.core.logDir, "preview_%s.log" % str(self.compPos)
+ )
+ log.debug("Creating FFmpeg process (log at %s)" % logFilename)
+ with open(logFilename, "w") as logf:
+ logf.write(" ".join(command) + "\n\n")
+ with open(logFilename, "a") as logf:
+ self.previewPipe = openPipe(
+ command,
+ stdin=subprocess.DEVNULL,
+ stdout=subprocess.PIPE,
+ stderr=logf,
+ bufsize=10**8,
+ )
+ else:
+ self.previewPipe = openPipe(
+ command,
+ stdin=subprocess.DEVNULL,
+ stdout=subprocess.PIPE,
+ stderr=subprocess.DEVNULL,
+ bufsize=10**8,
+ )
+ byteFrame = self.previewPipe.stdout.read(self.chunkSize)
+ closePipe(self.previewPipe)
+
+ frame = self.finalizeFrame(byteFrame)
+ return frame
+
+ def makeFfmpegFilter(self, preview=False, startPt=0):
+ """Makes final FFmpeg filter command"""
+
+ def getFilterComplexCommand():
+ """Inner function that creates the final, complex part of the filter command"""
+ nonlocal self
+ genericPreview = self.settings.value("pref_genericPreview")
+
+ def getFilterComplexCommandForType():
+ """Determine portion of filter command that changes depending on selected type"""
+ nonlocal self
+ if preview:
+ w, h = self.previewSize
+ else:
+ w, h = (self.width, self.height)
+ color = self.page.comboBox_color.currentText().lower()
+
+ if self.filterType == 0: # Spectrum
+ if self.amplitude == 0:
+ amplitude = "sqrt"
+ elif self.amplitude == 1:
+ amplitude = "cbrt"
+ elif self.amplitude == 2:
+ amplitude = "4thrt"
+ elif self.amplitude == 3:
+ amplitude = "5thrt"
+ elif self.amplitude == 4:
+ amplitude = "lin"
+ elif self.amplitude == 5:
+ amplitude = "log"
+ filter_ = (
+ f"showspectrum=s={w}x{h}:"
+ "slide=scroll:"
+ f"win_func={self.page.comboBox_window.currentText()}:"
+ f"color={color}:"
+ f"scale={amplitude},"
+ "colorkey=color=black:"
+ "similarity=0.1:blend=0.5"
+ )
+ elif self.filterType == 1: # Histogram
+ if self.amplitude1 == 0:
+ amplitude = "log"
+ elif self.amplitude1 == 1:
+ amplitude = "lin"
+ if self.display == 0:
+ display = "log"
+ elif self.display == 1:
+ display = "sqrt"
+ elif self.display == 2:
+ display = "cbrt"
+ elif self.display == 3:
+ display = "lin"
+ elif self.display == 4:
+ display = "rlog"
+ filter_ = (
+ f'ahistogram=r={str(self.settings.value("outputFrameRate"))}:'
+ f"s={w}x{h}:"
+ "dmode=separate:"
+ f"ascale={amplitude}:"
+ f"scale={display}"
+ )
+ elif self.filterType == 2: # Vector Scope
+ if self.amplitude2 == 0:
+ amplitude = "log"
+ elif self.amplitude2 == 1:
+ amplitude = "sqrt"
+ elif self.amplitude2 == 2:
+ amplitude = "cbrt"
+ elif self.amplitude2 == 3:
+ amplitude = "lin"
+ m = self.page.comboBox_mode.currentText()
+ filter_ = (
+ f"avectorscope=s={w}x{h}:"
+ f'draw={"line" if self.draw else "dot"}:'
+ f"m={m}:"
+ f"scale={amplitude}:"
+ f"zoom={str(self.zoom)}"
+ )
+ elif self.filterType == 3: # Musical Scale
+ filter_ = (
+ f'showcqt=r={str(self.settings.value("outputFrameRate"))}:'
+ f"s={w}x{h}:"
+ "count=30:"
+ "text=0:"
+ f"tc={str(self.tc)},"
+ "colorkey=color=black:"
+ "similarity=0.1:blend=0.5"
+ )
+ elif self.filterType == 4: # Phase
+ filter_ = (
+ f'aphasemeter=r={str(self.settings.value("outputFrameRate"))}:'
+ f"s={w}x{h}:"
+ "video=1 [atrash][vtmp1]; "
+ "[atrash] anullsink; "
+ "[vtmp1] colorkey=color=black:"
+ "similarity=0.1:blend=0.5, "
+ "crop=in_w/8:in_h:(in_w/8)*7:0 "
+ )
+ return filter_
+
+ if self.filterType < 2:
+ exampleSnd = exampleSound("freq")
+ elif self.filterType == 2 or self.filterType == 4:
+ exampleSnd = exampleSound("stereo")
+ elif self.filterType == 3:
+ exampleSnd = exampleSound("white")
+ compression = "compand=gain=4," if self.compress else ""
+ aformat = (
+ "aformat=channel_layouts=mono,"
+ if self.mono and self.filterType not in (2, 4)
+ else ""
+ )
+ filter_ = getFilterComplexCommandForType()
+ hflip = "hflip, " if self.mirror else ""
+ trim = (
+ "trim=start=%s:end=%s, "
+ % (
+ "{0:.3f}".format(startPt + 12),
+ "{0:.3f}".format(startPt + 12.5),
+ )
+ if preview
+ else ""
+ )
+ scale_ = "scale=%sx%s" % scale(self.scale, self.width, self.height, str)
+ hue = (
+ ", hue=h=%s:s=10" % str(self.hue)
+ if self.hue > 0 and self.filterType != 3
+ else ""
+ )
+ convolution = (
+ ", convolution=-2 -1 0 -1 1 1 0 1 2:-2 -1 0 -1 1 1 0 1 2:-2 -1 0 -1 1 1 0 1 2:-2 -1 0 -1 1 1 0 1 2"
+ if self.filterType == 3
+ else ""
+ )
+
+ return (
+ f"{exampleSnd if preview and genericPreview else '[0:a] '}"
+ f"{compression}{aformat}{filter_} [v1]; "
+ f"[v1] {hflip}{trim}{scale_}{hue}{convolution} [v]"
+ )
+
+ return [
+ "-filter_complex",
+ getFilterComplexCommand(),
+ "-map",
+ "[v]",
+ ]
+
+ def updateChunksize(self):
+ width, height = scale(self.scale, self.width, self.height, int)
+ oldChunkSize = int(self.chunkSize)
+ self.chunkSize = 4 * width * height
+ changed = self.chunkSize != oldChunkSize
+ return changed
+
+ def finalizeFrame(self, imageData):
+ try:
+ image = Image.frombytes(
+ "RGBA",
+ scale(self.scale, self.width, self.height, int),
+ imageData,
+ )
+ self._image = image
+ except ValueError:
+ image = self._image
+
+ frame = BlankFrame(self.width, self.height)
+ frame.paste(image, box=(self.x, self.y))
+ return frame
diff --git a/src/avp/components/spectrum.ui b/src/avp/components/spectrum.ui
new file mode 100644
index 0000000..c6a8a15
--- /dev/null
+++ b/src/avp/components/spectrum.ui
@@ -0,0 +1,946 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<ui version="4.0">
+ <class>Form</class>
+ <widget class="QWidget" name="Form">
+ <property name="geometry">
+ <rect>
+ <x>0</x>
+ <y>0</y>
+ <width>586</width>
+ <height>197</height>
+ </rect>
+ </property>
+ <property name="sizePolicy">
+ <sizepolicy hsizetype="Preferred" vsizetype="Preferred">
+ <horstretch>0</horstretch>
+ <verstretch>0</verstretch>
+ </sizepolicy>
+ </property>
+ <property name="minimumSize">
+ <size>
+ <width>0</width>
+ <height>197</height>
+ </size>
+ </property>
+ <property name="windowTitle">
+ <string>Form</string>
+ </property>
+ <layout class="QVBoxLayout" name="verticalLayout_2">
+ <item>
+ <layout class="QVBoxLayout" name="verticalLayout">
+ <property name="leftMargin">
+ <number>4</number>
+ </property>
+ <item>
+ <layout class="QHBoxLayout" name="horizontalLayout_5"/>
+ </item>
+ <item>
+ <layout class="QHBoxLayout" name="horizontalLayout_8">
+ <item>
+ <widget class="QLabel" name="label_4">
+ <property name="sizePolicy">
+ <sizepolicy hsizetype="Fixed" vsizetype="Preferred">
+ <horstretch>0</horstretch>
+ <verstretch>0</verstretch>
+ </sizepolicy>
+ </property>
+ <property name="text">
+ <string>Type</string>
+ </property>
+ </widget>
+ </item>
+ <item>
+ <widget class="QComboBox" name="comboBox_filterType">
+ <item>
+ <property name="text">
+ <string>Spectrum</string>
+ </property>
+ </item>
+ <item>
+ <property name="text">
+ <string>Histogram</string>
+ </property>
+ </item>
+ <item>
+ <property name="text">
+ <string>Vector Scope</string>
+ </property>
+ </item>
+ <item>
+ <property name="text">
+ <string>Musical Scale</string>
+ </property>
+ </item>
+ <item>
+ <property name="text">
+ <string>Phase</string>
+ </property>
+ </item>
+ </widget>
+ </item>
+ <item>
+ <spacer name="horizontalSpacer_9">
+ <property name="orientation">
+ <enum>Qt::Horizontal</enum>
+ </property>
+ <property name="sizeType">
+ <enum>QSizePolicy::Fixed</enum>
+ </property>
+ <property name="sizeHint" stdset="0">
+ <size>
+ <width>5</width>
+ <height>20</height>
+ </size>
+ </property>
+ </spacer>
+ </item>
+ <item>
+ <widget class="QLabel" name="label_xTitleAlign">
+ <property name="sizePolicy">
+ <sizepolicy hsizetype="Fixed" vsizetype="Preferred">
+ <horstretch>0</horstretch>
+ <verstretch>0</verstretch>
+ </sizepolicy>
+ </property>
+ <property name="text">
+ <string>X</string>
+ </property>
+ </widget>
+ </item>
+ <item>
+ <widget class="QSpinBox" name="spinBox_x">
+ <property name="sizePolicy">
+ <sizepolicy hsizetype="Fixed" vsizetype="Fixed">
+ <horstretch>0</horstretch>
+ <verstretch>0</verstretch>
+ </sizepolicy>
+ </property>
+ <property name="maximumSize">
+ <size>
+ <width>80</width>
+ <height>16777215</height>
+ </size>
+ </property>
+ <property name="minimum">
+ <number>-10000</number>
+ </property>
+ <property name="maximum">
+ <number>10000</number>
+ </property>
+ </widget>
+ </item>
+ <item>
+ <widget class="QLabel" name="label_yTitleAlign">
+ <property name="sizePolicy">
+ <sizepolicy hsizetype="Fixed" vsizetype="Preferred">
+ <horstretch>0</horstretch>
+ <verstretch>0</verstretch>
+ </sizepolicy>
+ </property>
+ <property name="text">
+ <string>Y</string>
+ </property>
+ </widget>
+ </item>
+ <item>
+ <widget class="QSpinBox" name="spinBox_y">
+ <property name="sizePolicy">
+ <sizepolicy hsizetype="Fixed" vsizetype="Fixed">
+ <horstretch>0</horstretch>
+ <verstretch>0</verstretch>
+ </sizepolicy>
+ </property>
+ <property name="maximumSize">
+ <size>
+ <width>80</width>
+ <height>16777215</height>
+ </size>
+ </property>
+ <property name="baseSize">
+ <size>
+ <width>0</width>
+ <height>0</height>
+ </size>
+ </property>
+ <property name="minimum">
+ <number>-10000</number>
+ </property>
+ <property name="maximum">
+ <number>10000</number>
+ </property>
+ <property name="value">
+ <number>0</number>
+ </property>
+ </widget>
+ </item>
+ </layout>
+ </item>
+ <item>
+ <layout class="QHBoxLayout" name="horizontalLayout_10">
+ <item>
+ <widget class="QCheckBox" name="checkBox_compress">
+ <property name="text">
+ <string>Compress</string>
+ </property>
+ </widget>
+ </item>
+ <item>
+ <widget class="QCheckBox" name="checkBox_mono">
+ <property name="text">
+ <string>Mono</string>
+ </property>
+ </widget>
+ </item>
+ <item>
+ <widget class="QCheckBox" name="checkBox_mirror">
+ <property name="text">
+ <string>Mirror</string>
+ </property>
+ </widget>
+ </item>
+ <item>
+ <spacer name="horizontalSpacer_2">
+ <property name="orientation">
+ <enum>Qt::Horizontal</enum>
+ </property>
+ <property name="sizeHint" stdset="0">
+ <size>
+ <width>40</width>
+ <height>20</height>
+ </size>
+ </property>
+ </spacer>
+ </item>
+ <item>
+ <widget class="QLabel" name="label_11">
+ <property name="text">
+ <string>Hue</string>
+ </property>
+ <property name="margin">
+ <number>4</number>
+ </property>
+ </widget>
+ </item>
+ <item>
+ <widget class="QSpinBox" name="spinBox_hue">
+ <property name="suffix">
+ <string>° </string>
+ </property>
+ <property name="maximum">
+ <number>359</number>
+ </property>
+ </widget>
+ </item>
+ <item>
+ <widget class="QLabel" name="label">
+ <property name="sizePolicy">
+ <sizepolicy hsizetype="Fixed" vsizetype="Preferred">
+ <horstretch>0</horstretch>
+ <verstretch>0</verstretch>
+ </sizepolicy>
+ </property>
+ <property name="text">
+ <string>Scale</string>
+ </property>
+ <property name="alignment">
+ <set>Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter</set>
+ </property>
+ </widget>
+ </item>
+ <item>
+ <widget class="QSpinBox" name="spinBox_scale">
+ <property name="buttonSymbols">
+ <enum>QAbstractSpinBox::UpDownArrows</enum>
+ </property>
+ <property name="suffix">
+ <string>%</string>
+ </property>
+ <property name="minimum">
+ <number>10</number>
+ </property>
+ <property name="maximum">
+ <number>400</number>
+ </property>
+ <property name="value">
+ <number>100</number>
+ </property>
+ </widget>
+ </item>
+ </layout>
+ </item>
+ <item>
+ <widget class="QStackedWidget" name="stackedWidget">
+ <property name="sizePolicy">
+ <sizepolicy hsizetype="Preferred" vsizetype="Preferred">
+ <horstretch>0</horstretch>
+ <verstretch>0</verstretch>
+ </sizepolicy>
+ </property>
+ <property name="autoFillBackground">
+ <bool>false</bool>
+ </property>
+ <property name="frameShape">
+ <enum>QFrame::NoFrame</enum>
+ </property>
+ <property name="frameShadow">
+ <enum>QFrame::Plain</enum>
+ </property>
+ <property name="currentIndex">
+ <number>0</number>
+ </property>
+ <widget class="QWidget" name="page">
+ <widget class="QWidget" name="verticalLayoutWidget">
+ <property name="geometry">
+ <rect>
+ <x>0</x>
+ <y>0</y>
+ <width>561</width>
+ <height>66</height>
+ </rect>
+ </property>
+ <layout class="QVBoxLayout" name="verticalLayout_3">
+ <property name="sizeConstraint">
+ <enum>QLayout::SetMaximumSize</enum>
+ </property>
+ <property name="rightMargin">
+ <number>0</number>
+ </property>
+ <item>
+ <layout class="QHBoxLayout" name="horizontalLayout_9">
+ <property name="sizeConstraint">
+ <enum>QLayout::SetDefaultConstraint</enum>
+ </property>
+ <item>
+ <widget class="QLabel" name="label_textColor">
+ <property name="sizePolicy">
+ <sizepolicy hsizetype="Fixed" vsizetype="Preferred">
+ <horstretch>0</horstretch>
+ <verstretch>0</verstretch>
+ </sizepolicy>
+ </property>
+ <property name="minimumSize">
+ <size>
+ <width>31</width>
+ <height>0</height>
+ </size>
+ </property>
+ <property name="text">
+ <string>Window</string>
+ </property>
+ <property name="margin">
+ <number>4</number>
+ </property>
+ </widget>
+ </item>
+ <item>
+ <widget class="QComboBox" name="comboBox_window">
+ <item>
+ <property name="text">
+ <string>hann</string>
+ </property>
+ </item>
+ <item>
+ <property name="text">
+ <string>gauss</string>
+ </property>
+ </item>
+ <item>
+ <property name="text">
+ <string>tukey</string>
+ </property>
+ </item>
+ <item>
+ <property name="text">
+ <string>dolph</string>
+ </property>
+ </item>
+ <item>
+ <property name="text">
+ <string>cauchy</string>
+ </property>
+ </item>
+ <item>
+ <property name="text">
+ <string>parzen</string>
+ </property>
+ </item>
+ <item>
+ <property name="text">
+ <string>poisson</string>
+ </property>
+ </item>
+ <item>
+ <property name="text">
+ <string>rect</string>
+ </property>
+ </item>
+ <item>
+ <property name="text">
+ <string>bartlett</string>
+ </property>
+ </item>
+ <item>
+ <property name="text">
+ <string>hanning</string>
+ </property>
+ </item>
+ <item>
+ <property name="text">
+ <string>hamming</string>
+ </property>
+ </item>
+ <item>
+ <property name="text">
+ <string>blackman</string>
+ </property>
+ </item>
+ <item>
+ <property name="text">
+ <string>welch</string>
+ </property>
+ </item>
+ <item>
+ <property name="text">
+ <string>flattop</string>
+ </property>
+ </item>
+ <item>
+ <property name="text">
+ <string>bharris</string>
+ </property>
+ </item>
+ <item>
+ <property name="text">
+ <string>bnuttall</string>
+ </property>
+ </item>
+ <item>
+ <property name="text">
+ <string>lanczos</string>
+ </property>
+ </item>
+ </widget>
+ </item>
+ <item>
+ <widget class="QLabel" name="label_3">
+ <property name="sizePolicy">
+ <sizepolicy hsizetype="Fixed" vsizetype="Preferred">
+ <horstretch>0</horstretch>
+ <verstretch>0</verstretch>
+ </sizepolicy>
+ </property>
+ <property name="text">
+ <string>Amplitude</string>
+ </property>
+ <property name="margin">
+ <number>4</number>
+ </property>
+ </widget>
+ </item>
+ <item>
+ <widget class="QComboBox" name="comboBox_amplitude0">
+ <item>
+ <property name="text">
+ <string>Square root</string>
+ </property>
+ </item>
+ <item>
+ <property name="text">
+ <string>Cubic root</string>
+ </property>
+ </item>
+ <item>
+ <property name="text">
+ <string>4thrt</string>
+ </property>
+ </item>
+ <item>
+ <property name="text">
+ <string>5thrt</string>
+ </property>
+ </item>
+ <item>
+ <property name="text">
+ <string>Linear</string>
+ </property>
+ </item>
+ <item>
+ <property name="text">
+ <string>Logarithmic</string>
+ </property>
+ </item>
+ </widget>
+ </item>
+ <item>
+ <spacer name="horizontalSpacer_4">
+ <property name="orientation">
+ <enum>Qt::Horizontal</enum>
+ </property>
+ <property name="sizeType">
+ <enum>QSizePolicy::MinimumExpanding</enum>
+ </property>
+ <property name="sizeHint" stdset="0">
+ <size>
+ <width>10</width>
+ <height>20</height>
+ </size>
+ </property>
+ </spacer>
+ </item>
+ </layout>
+ </item>
+ <item>
+ <layout class="QHBoxLayout" name="horizontalLayout">
+ <item>
+ <widget class="QLabel" name="label_2">
+ <property name="sizePolicy">
+ <sizepolicy hsizetype="Fixed" vsizetype="Preferred">
+ <horstretch>0</horstretch>
+ <verstretch>0</verstretch>
+ </sizepolicy>
+ </property>
+ <property name="text">
+ <string>Color </string>
+ </property>
+ <property name="margin">
+ <number>4</number>
+ </property>
+ </widget>
+ </item>
+ <item>
+ <widget class="QComboBox" name="comboBox_color">
+ <item>
+ <property name="text">
+ <string>Channel</string>
+ </property>
+ </item>
+ <item>
+ <property name="text">
+ <string>Intensity</string>
+ </property>
+ </item>
+ <item>
+ <property name="text">
+ <string>Rainbow</string>
+ </property>
+ </item>
+ <item>
+ <property name="text">
+ <string>Moreland</string>
+ </property>
+ </item>
+ <item>
+ <property name="text">
+ <string>Nebulae</string>
+ </property>
+ </item>
+ <item>
+ <property name="text">
+ <string>Fire</string>
+ </property>
+ </item>
+ <item>
+ <property name="text">
+ <string>Fiery</string>
+ </property>
+ </item>
+ <item>
+ <property name="text">
+ <string>Fruit</string>
+ </property>
+ </item>
+ <item>
+ <property name="text">
+ <string>Cool</string>
+ </property>
+ </item>
+ </widget>
+ </item>
+ <item>
+ <spacer name="horizontalSpacer_3">
+ <property name="orientation">
+ <enum>Qt::Horizontal</enum>
+ </property>
+ <property name="sizeType">
+ <enum>QSizePolicy::MinimumExpanding</enum>
+ </property>
+ <property name="sizeHint" stdset="0">
+ <size>
+ <width>10</width>
+ <height>20</height>
+ </size>
+ </property>
+ </spacer>
+ </item>
+ </layout>
+ </item>
+ </layout>
+ </widget>
+ </widget>
+ <widget class="QWidget" name="page_2">
+ <widget class="QWidget" name="verticalLayoutWidget_2">
+ <property name="geometry">
+ <rect>
+ <x>-1</x>
+ <y>-1</y>
+ <width>561</width>
+ <height>31</height>
+ </rect>
+ </property>
+ <layout class="QVBoxLayout" name="verticalLayout_4">
+ <item>
+ <layout class="QHBoxLayout" name="horizontalLayout_2">
+ <item>
+ <widget class="QLabel" name="label_6">
+ <property name="sizePolicy">
+ <sizepolicy hsizetype="Fixed" vsizetype="Preferred">
+ <horstretch>0</horstretch>
+ <verstretch>0</verstretch>
+ </sizepolicy>
+ </property>
+ <property name="text">
+ <string>Display Scale</string>
+ </property>
+ <property name="margin">
+ <number>4</number>
+ </property>
+ </widget>
+ </item>
+ <item>
+ <widget class="QComboBox" name="comboBox_display">
+ <item>
+ <property name="text">
+ <string>Logarithmic</string>
+ </property>
+ </item>
+ <item>
+ <property name="text">
+ <string>Square root</string>
+ </property>
+ </item>
+ <item>
+ <property name="text">
+ <string>Cubic root</string>
+ </property>
+ </item>
+ <item>
+ <property name="text">
+ <string>Linear</string>
+ </property>
+ </item>
+ <item>
+ <property name="text">
+ <string>Reverse Log</string>
+ </property>
+ </item>
+ </widget>
+ </item>
+ <item>
+ <widget class="QLabel" name="label_5">
+ <property name="sizePolicy">
+ <sizepolicy hsizetype="Fixed" vsizetype="Preferred">
+ <horstretch>0</horstretch>
+ <verstretch>0</verstretch>
+ </sizepolicy>
+ </property>
+ <property name="text">
+ <string>Amplitude</string>
+ </property>
+ <property name="margin">
+ <number>4</number>
+ </property>
+ </widget>
+ </item>
+ <item>
+ <widget class="QComboBox" name="comboBox_amplitude1">
+ <item>
+ <property name="text">
+ <string>Logarithmic</string>
+ </property>
+ </item>
+ <item>
+ <property name="text">
+ <string>Linear</string>
+ </property>
+ </item>
+ </widget>
+ </item>
+ <item>
+ <spacer name="horizontalSpacer">
+ <property name="orientation">
+ <enum>Qt::Horizontal</enum>
+ </property>
+ <property name="sizeType">
+ <enum>QSizePolicy::Minimum</enum>
+ </property>
+ <property name="sizeHint" stdset="0">
+ <size>
+ <width>40</width>
+ <height>20</height>
+ </size>
+ </property>
+ </spacer>
+ </item>
+ </layout>
+ </item>
+ </layout>
+ </widget>
+ </widget>
+ <widget class="QWidget" name="page_3">
+ <widget class="QWidget" name="verticalLayoutWidget_3">
+ <property name="geometry">
+ <rect>
+ <x>-1</x>
+ <y>-1</y>
+ <width>585</width>
+ <height>64</height>
+ </rect>
+ </property>
+ <layout class="QVBoxLayout" name="verticalLayout_5">
+ <item>
+ <layout class="QHBoxLayout" name="horizontalLayout_3">
+ <item>
+ <widget class="QLabel" name="label_9">
+ <property name="text">
+ <string>Mode</string>
+ </property>
+ </widget>
+ </item>
+ <item>
+ <widget class="QComboBox" name="comboBox_mode">
+ <item>
+ <property name="text">
+ <string>lissajous</string>
+ </property>
+ </item>
+ <item>
+ <property name="text">
+ <string>lissajous_xy</string>
+ </property>
+ </item>
+ <item>
+ <property name="text">
+ <string>polar</string>
+ </property>
+ </item>
+ </widget>
+ </item>
+ <item>
+ <widget class="QLabel" name="label_7">
+ <property name="sizePolicy">
+ <sizepolicy hsizetype="Fixed" vsizetype="Preferred">
+ <horstretch>0</horstretch>
+ <verstretch>0</verstretch>
+ </sizepolicy>
+ </property>
+ <property name="text">
+ <string>Amplitude</string>
+ </property>
+ <property name="margin">
+ <number>4</number>
+ </property>
+ </widget>
+ </item>
+ <item>
+ <widget class="QComboBox" name="comboBox_amplitude2">
+ <item>
+ <property name="text">
+ <string>Linear</string>
+ </property>
+ </item>
+ <item>
+ <property name="text">
+ <string>Square root</string>
+ </property>
+ </item>
+ <item>
+ <property name="text">
+ <string>Cubic root</string>
+ </property>
+ </item>
+ <item>
+ <property name="text">
+ <string>Logarithmic</string>
+ </property>
+ </item>
+ </widget>
+ </item>
+ <item>
+ <spacer name="horizontalSpacer_5">
+ <property name="orientation">
+ <enum>Qt::Horizontal</enum>
+ </property>
+ <property name="sizeHint" stdset="0">
+ <size>
+ <width>40</width>
+ <height>20</height>
+ </size>
+ </property>
+ </spacer>
+ </item>
+ </layout>
+ </item>
+ <item>
+ <layout class="QHBoxLayout" name="horizontalLayout_6">
+ <item>
+ <widget class="QLabel" name="label_8">
+ <property name="sizePolicy">
+ <sizepolicy hsizetype="Fixed" vsizetype="Preferred">
+ <horstretch>0</horstretch>
+ <verstretch>0</verstretch>
+ </sizepolicy>
+ </property>
+ <property name="text">
+ <string>Zoom</string>
+ </property>
+ <property name="margin">
+ <number>4</number>
+ </property>
+ </widget>
+ </item>
+ <item>
+ <widget class="QSpinBox" name="spinBox_zoom">
+ <property name="minimum">
+ <number>1</number>
+ </property>
+ <property name="maximum">
+ <number>10</number>
+ </property>
+ </widget>
+ </item>
+ <item>
+ <widget class="QCheckBox" name="checkBox_draw">
+ <property name="text">
+ <string>Line</string>
+ </property>
+ </widget>
+ </item>
+ <item>
+ <spacer name="horizontalSpacer_6">
+ <property name="orientation">
+ <enum>Qt::Horizontal</enum>
+ </property>
+ <property name="sizeHint" stdset="0">
+ <size>
+ <width>40</width>
+ <height>20</height>
+ </size>
+ </property>
+ </spacer>
+ </item>
+ </layout>
+ </item>
+ </layout>
+ </widget>
+ </widget>
+ <widget class="QWidget" name="page_4">
+ <widget class="QWidget" name="verticalLayoutWidget_4">
+ <property name="geometry">
+ <rect>
+ <x>0</x>
+ <y>0</y>
+ <width>561</width>
+ <height>31</height>
+ </rect>
+ </property>
+ <layout class="QVBoxLayout" name="verticalLayout_6">
+ <item>
+ <layout class="QHBoxLayout" name="horizontalLayout_7">
+ <item>
+ <widget class="QLabel" name="label_10">
+ <property name="sizePolicy">
+ <sizepolicy hsizetype="Fixed" vsizetype="Preferred">
+ <horstretch>0</horstretch>
+ <verstretch>0</verstretch>
+ </sizepolicy>
+ </property>
+ <property name="text">
+ <string>Timeclamp</string>
+ </property>
+ <property name="margin">
+ <number>4</number>
+ </property>
+ </widget>
+ </item>
+ <item>
+ <widget class="QDoubleSpinBox" name="spinBox_tc">
+ <property name="suffix">
+ <string>s</string>
+ </property>
+ <property name="decimals">
+ <number>3</number>
+ </property>
+ <property name="minimum">
+ <double>0.002000000000000</double>
+ </property>
+ <property name="maximum">
+ <double>1.000000000000000</double>
+ </property>
+ <property name="singleStep">
+ <double>0.010000000000000</double>
+ </property>
+ <property name="value">
+ <double>0.017000000000000</double>
+ </property>
+ </widget>
+ </item>
+ <item>
+ <spacer name="horizontalSpacer_7">
+ <property name="orientation">
+ <enum>Qt::Horizontal</enum>
+ </property>
+ <property name="sizeHint" stdset="0">
+ <size>
+ <width>40</width>
+ <height>20</height>
+ </size>
+ </property>
+ </spacer>
+ </item>
+ </layout>
+ </item>
+ </layout>
+ </widget>
+ </widget>
+ <widget class="QWidget" name="page_5">
+ <widget class="QWidget" name="verticalLayoutWidget_5">
+ <property name="geometry">
+ <rect>
+ <x>0</x>
+ <y>0</y>
+ <width>551</width>
+ <height>31</height>
+ </rect>
+ </property>
+ <layout class="QVBoxLayout" name="verticalLayout_7">
+ <item>
+ <layout class="QHBoxLayout" name="horizontalLayout_11"/>
+ </item>
+ </layout>
+ </widget>
+ </widget>
+ </widget>
+ </item>
+ </layout>
+ </item>
+ <item>
+ <spacer name="verticalSpacer">
+ <property name="orientation">
+ <enum>Qt::Vertical</enum>
+ </property>
+ <property name="sizeType">
+ <enum>QSizePolicy::Fixed</enum>
+ </property>
+ <property name="sizeHint" stdset="0">
+ <size>
+ <width>20</width>
+ <height>10</height>
+ </size>
+ </property>
+ </spacer>
+ </item>
+ </layout>
+ </widget>
+ <resources/>
+ <connections/>
+</ui>
diff --git a/src/avp/components/text.py b/src/avp/components/text.py
new file mode 100644
index 0000000..40c981a
--- /dev/null
+++ b/src/avp/components/text.py
@@ -0,0 +1,218 @@
+from PIL import ImageEnhance, ImageFilter, ImageChops
+from PyQt6.QtGui import QColor, QFont
+from PyQt6 import QtGui, QtCore, QtWidgets
+import os
+import logging
+
+from ..component import Component
+from ..toolkit.frame import FramePainter, PaintColor
+
+log = logging.getLogger("AVP.Components.Text")
+
+
+class Component(Component):
+ name = "Title Text"
+ version = "1.0.1"
+
+ def widget(self, *args):
+ super().widget(*args)
+ self.title = "Text"
+ self.alignment = 1
+ self.titleFont = QFont()
+ self.fontSize = self.height / 13.5
+
+ self.page.comboBox_textAlign.addItem("Left")
+ self.page.comboBox_textAlign.addItem("Middle")
+ self.page.comboBox_textAlign.addItem("Right")
+ self.page.comboBox_textAlign.setCurrentIndex(int(self.alignment))
+ self.page.spinBox_fontSize.setValue(int(self.fontSize))
+ self.page.lineEdit_title.setText(self.title)
+ self.page.pushButton_center.clicked.connect(self.centerXY)
+
+ self.page.fontComboBox_titleFont.currentFontChanged.connect(
+ self._sendUpdateSignal
+ )
+ # The QFontComboBox must be connected directly to the Qt Signal
+ # which triggers the preview to update.
+ # This unfortunately makes changing the font into a non-undoable action.
+ # Must be something broken in the conversion to a ComponentAction
+
+ self.trackWidgets(
+ {
+ "textColor": self.page.lineEdit_textColor,
+ "title": self.page.lineEdit_title,
+ "alignment": self.page.comboBox_textAlign,
+ "fontSize": self.page.spinBox_fontSize,
+ "xPosition": self.page.spinBox_xTextAlign,
+ "yPosition": self.page.spinBox_yTextAlign,
+ "fontStyle": self.page.comboBox_fontStyle,
+ "stroke": self.page.spinBox_stroke,
+ "strokeColor": self.page.lineEdit_strokeColor,
+ "shadow": self.page.checkBox_shadow,
+ "shadX": self.page.spinBox_shadX,
+ "shadY": self.page.spinBox_shadY,
+ "shadBlur": self.page.spinBox_shadBlur,
+ },
+ colorWidgets={
+ "textColor": self.page.pushButton_textColor,
+ "strokeColor": self.page.pushButton_strokeColor,
+ },
+ relativeWidgets=[
+ "xPosition",
+ "yPosition",
+ "fontSize",
+ "stroke",
+ "shadX",
+ "shadY",
+ "shadBlur",
+ ],
+ )
+ self.centerXY()
+
+ def update(self):
+ self.titleFont = self.page.fontComboBox_titleFont.currentFont()
+ if self.page.checkBox_shadow.isChecked():
+ self.page.label_shadX.setHidden(False)
+ self.page.spinBox_shadX.setHidden(False)
+ self.page.spinBox_shadY.setHidden(False)
+ self.page.label_shadBlur.setHidden(False)
+ self.page.spinBox_shadBlur.setHidden(False)
+ else:
+ self.page.label_shadX.setHidden(True)
+ self.page.spinBox_shadX.setHidden(True)
+ self.page.spinBox_shadY.setHidden(True)
+ self.page.label_shadBlur.setHidden(True)
+ self.page.spinBox_shadBlur.setHidden(True)
+
+ def centerXY(self):
+ self.setRelativeWidget("xPosition", 0.5)
+ self.setRelativeWidget("yPosition", 0.521)
+
+ def getXY(self):
+ """Returns true x, y after considering alignment settings"""
+ fm = QtGui.QFontMetrics(self.titleFont)
+ text_width = fm.boundingRect(self.title).width()
+ x = self.pixelValForAttr("xPosition")
+
+ if self.alignment == 1: # Middle
+ offset = int(text_width / 2)
+ elif self.alignment == 2: # Right
+ offset = text_width
+ else:
+ raise ValueError(f"Alignment value {self.alignment} unknown")
+
+ x -= offset
+
+ return x, self.yPosition
+
+ def loadPreset(self, pr, *args):
+ super().loadPreset(pr, *args)
+
+ font = QFont()
+ font.fromString(pr["titleFont"])
+ self.page.fontComboBox_titleFont.setCurrentFont(font)
+
+ def savePreset(self):
+ saveValueStore = super().savePreset()
+ saveValueStore["titleFont"] = self.titleFont.toString()
+ return saveValueStore
+
+ def previewRender(self):
+ return self.addText(self.width, self.height)
+
+ def properties(self):
+ props = ["static"]
+ if not self.title:
+ props.append("error")
+ return props
+
+ def error(self):
+ return "No text provided."
+
+ def frameRender(self, frameNo):
+ return self.addText(self.width, self.height)
+
+ def addText(self, width, height):
+ font = self.titleFont
+ font.setPixelSize(self.fontSize)
+ font.setStyle(QFont.Style.StyleNormal)
+ font.setWeight(QFont.Weight.Normal)
+ font.setCapitalization(QFont.Capitalization.MixedCase)
+ if self.fontStyle == 1:
+ font.setWeight(QFont.Weight.DemiBold)
+ if self.fontStyle == 2:
+ font.setWeight(QFont.Weight.Bold)
+ elif self.fontStyle == 3:
+ font.setStyle(QFont.Style.StyleItalic)
+ elif self.fontStyle == 4:
+ font.setWeight(QFont.Weight.Bold)
+ font.setStyle(QFont.Style.StyleItalic)
+ elif self.fontStyle == 5:
+ font.setStyle(QFont.Style.StyleOblique)
+ elif self.fontStyle == 6:
+ font.setCapitalization(QFont.Capitalization.SmallCaps)
+
+ image = FramePainter(width, height)
+ x, y = self.getXY()
+ log.debug("Text position translates to %s, %s", x, y)
+ if self.stroke > 0:
+ outliner = QtGui.QPainterPathStroker()
+ outliner.setWidth(self.stroke)
+ path = QtGui.QPainterPath()
+ if self.fontStyle == 6:
+ # PathStroker ignores smallcaps so we need this weird hack
+ path.addText(x, y, font, self.title[0])
+ fm = QtGui.QFontMetrics(font)
+ newX = x + fm.boundingRect(self.title[0]).width()
+ strokeFont = self.page.fontComboBox_titleFont.currentFont()
+ strokeFont.setCapitalization(QFont.Capitalization.SmallCaps)
+ strokeFont.setPixelSize(int((self.fontSize / 7) * 5))
+ strokeFont.setLetterSpacing(QFont.SpacingType.PercentageSpacing, 139)
+ path.addText(newX, y, strokeFont, self.title[1:])
+ else:
+ path.addText(x, y, font, self.title)
+ path = outliner.createStroke(path)
+ image.setPen(QtCore.Qt.PenStyle.NoPen)
+ image.setBrush(PaintColor(*self.strokeColor))
+ image.drawPath(path)
+
+ image.setFont(font)
+ image.setPen(self.textColor)
+ image.drawText(x, y, self.title)
+
+ # turn QImage into Pillow frame
+ frame = image.finalize()
+ if self.shadow:
+ shadImg = ImageEnhance.Contrast(frame).enhance(0.0)
+ shadImg = shadImg.filter(ImageFilter.GaussianBlur(self.shadBlur))
+ shadImg = ImageChops.offset(shadImg, self.shadX, self.shadY)
+ shadImg.paste(frame, box=(0, 0), mask=frame)
+ frame = shadImg
+
+ return frame
+
+ def commandHelp(self):
+ print("Enter a string to use as centred white text:")
+ print(' "title=User Error"')
+ print("Specify a text color:\n color=255,255,255")
+ print("Set custom x, y position:\n x=500 y=500")
+
+ def command(self, arg):
+ if "=" in arg:
+ key, arg = arg.split("=", 1)
+ if key == "color":
+ self.page.lineEdit_textColor.setText(arg)
+ return
+ elif key == "size":
+ self.page.spinBox_fontSize.setValue(int(arg))
+ return
+ elif key == "x":
+ self.page.spinBox_xTextAlign.setValue(int(arg))
+ return
+ elif key == "y":
+ self.page.spinBox_yTextAlign.setValue(int(arg))
+ return
+ elif key == "title":
+ self.page.lineEdit_title.setText(arg)
+ return
+ super().command(arg)
diff --git a/src/avp/components/text.ui b/src/avp/components/text.ui
new file mode 100644
index 0000000..b62e0ed
--- /dev/null
+++ b/src/avp/components/text.ui
@@ -0,0 +1,671 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<ui version="4.0">
+ <class>Form</class>
+ <widget class="QWidget" name="Form">
+ <property name="geometry">
+ <rect>
+ <x>0</x>
+ <y>0</y>
+ <width>586</width>
+ <height>197</height>
+ </rect>
+ </property>
+ <property name="windowTitle">
+ <string>Form</string>
+ </property>
+ <layout class="QVBoxLayout" name="verticalLayout_2">
+ <item>
+ <layout class="QVBoxLayout" name="verticalLayout">
+ <property name="spacing">
+ <number>6</number>
+ </property>
+ <property name="sizeConstraint">
+ <enum>QLayout::SetDefaultConstraint</enum>
+ </property>
+ <property name="leftMargin">
+ <number>4</number>
+ </property>
+ <item>
+ <layout class="QHBoxLayout" name="horizontalLayout">
+ <item>
+ <widget class="QLabel" name="label_title">
+ <property name="text">
+ <string>Title</string>
+ </property>
+ </widget>
+ </item>
+ <item>
+ <widget class="QLineEdit" name="lineEdit_title">
+ <property name="sizePolicy">
+ <sizepolicy hsizetype="Expanding" vsizetype="Fixed">
+ <horstretch>0</horstretch>
+ <verstretch>0</verstretch>
+ </sizepolicy>
+ </property>
+ <property name="minimumSize">
+ <size>
+ <width>0</width>
+ <height>0</height>
+ </size>
+ </property>
+ <property name="text">
+ <string>Testing New GUI</string>
+ </property>
+ </widget>
+ </item>
+ <item>
+ <widget class="QLabel" name="label">
+ <property name="sizePolicy">
+ <sizepolicy hsizetype="Minimum" vsizetype="Preferred">
+ <horstretch>0</horstretch>
+ <verstretch>0</verstretch>
+ </sizepolicy>
+ </property>
+ <property name="text">
+ <string>Font</string>
+ </property>
+ </widget>
+ </item>
+ <item>
+ <widget class="QFontComboBox" name="fontComboBox_titleFont">
+ <property name="sizePolicy">
+ <sizepolicy hsizetype="Preferred" vsizetype="Fixed">
+ <horstretch>0</horstretch>
+ <verstretch>0</verstretch>
+ </sizepolicy>
+ </property>
+ <property name="minimumSize">
+ <size>
+ <width>0</width>
+ <height>0</height>
+ </size>
+ </property>
+ </widget>
+ </item>
+ </layout>
+ </item>
+ <item>
+ <layout class="QHBoxLayout" name="horizontalLayout_7">
+ <property name="leftMargin">
+ <number>0</number>
+ </property>
+ <item>
+ <widget class="QLabel" name="label_textLayout">
+ <property name="sizePolicy">
+ <sizepolicy hsizetype="Fixed" vsizetype="Preferred">
+ <horstretch>0</horstretch>
+ <verstretch>0</verstretch>
+ </sizepolicy>
+ </property>
+ <property name="text">
+ <string>Text Layout</string>
+ </property>
+ </widget>
+ </item>
+ <item>
+ <widget class="QComboBox" name="comboBox_textAlign">
+ <property name="sizePolicy">
+ <sizepolicy hsizetype="Preferred" vsizetype="Fixed">
+ <horstretch>0</horstretch>
+ <verstretch>0</verstretch>
+ </sizepolicy>
+ </property>
+ <property name="maximumSize">
+ <size>
+ <width>100</width>
+ <height>16777215</height>
+ </size>
+ </property>
+ </widget>
+ </item>
+ <item>
+ <spacer name="horizontalSpacer_2">
+ <property name="orientation">
+ <enum>Qt::Horizontal</enum>
+ </property>
+ <property name="sizeType">
+ <enum>QSizePolicy::Fixed</enum>
+ </property>
+ <property name="sizeHint" stdset="0">
+ <size>
+ <width>5</width>
+ <height>20</height>
+ </size>
+ </property>
+ </spacer>
+ </item>
+ <item>
+ <widget class="QPushButton" name="pushButton_center">
+ <property name="sizePolicy">
+ <sizepolicy hsizetype="Preferred" vsizetype="Fixed">
+ <horstretch>0</horstretch>
+ <verstretch>0</verstretch>
+ </sizepolicy>
+ </property>
+ <property name="text">
+ <string>Center Text</string>
+ </property>
+ </widget>
+ </item>
+ <item>
+ <spacer name="horizontalSpacer_6">
+ <property name="orientation">
+ <enum>Qt::Horizontal</enum>
+ </property>
+ <property name="sizeType">
+ <enum>QSizePolicy::Fixed</enum>
+ </property>
+ <property name="sizeHint" stdset="0">
+ <size>
+ <width>5</width>
+ <height>20</height>
+ </size>
+ </property>
+ </spacer>
+ </item>
+ <item>
+ <widget class="QLabel" name="label_xTitleAlign">
+ <property name="sizePolicy">
+ <sizepolicy hsizetype="Fixed" vsizetype="Preferred">
+ <horstretch>0</horstretch>
+ <verstretch>0</verstretch>
+ </sizepolicy>
+ </property>
+ <property name="text">
+ <string>X</string>
+ </property>
+ </widget>
+ </item>
+ <item>
+ <widget class="QSpinBox" name="spinBox_xTextAlign">
+ <property name="sizePolicy">
+ <sizepolicy hsizetype="Preferred" vsizetype="Fixed">
+ <horstretch>0</horstretch>
+ <verstretch>0</verstretch>
+ </sizepolicy>
+ </property>
+ <property name="maximumSize">
+ <size>
+ <width>50</width>
+ <height>16777215</height>
+ </size>
+ </property>
+ <property name="baseSize">
+ <size>
+ <width>0</width>
+ <height>0</height>
+ </size>
+ </property>
+ <property name="minimum">
+ <number>0</number>
+ </property>
+ <property name="maximum">
+ <number>999999999</number>
+ </property>
+ <property name="value">
+ <number>0</number>
+ </property>
+ </widget>
+ </item>
+ <item>
+ <widget class="QLabel" name="label_yTitleAlign">
+ <property name="sizePolicy">
+ <sizepolicy hsizetype="Fixed" vsizetype="Preferred">
+ <horstretch>0</horstretch>
+ <verstretch>0</verstretch>
+ </sizepolicy>
+ </property>
+ <property name="text">
+ <string>Y</string>
+ </property>
+ </widget>
+ </item>
+ <item>
+ <widget class="QSpinBox" name="spinBox_yTextAlign">
+ <property name="sizePolicy">
+ <sizepolicy hsizetype="Preferred" vsizetype="Fixed">
+ <horstretch>0</horstretch>
+ <verstretch>0</verstretch>
+ </sizepolicy>
+ </property>
+ <property name="maximumSize">
+ <size>
+ <width>50</width>
+ <height>16777215</height>
+ </size>
+ </property>
+ <property name="maximum">
+ <number>999999999</number>
+ </property>
+ </widget>
+ </item>
+ </layout>
+ </item>
+ <item>
+ <layout class="QHBoxLayout" name="horizontalLayout_8">
+ <item>
+ <widget class="QLabel" name="label_textColor">
+ <property name="sizePolicy">
+ <sizepolicy hsizetype="Fixed" vsizetype="Preferred">
+ <horstretch>0</horstretch>
+ <verstretch>0</verstretch>
+ </sizepolicy>
+ </property>
+ <property name="maximumSize">
+ <size>
+ <width>16777215</width>
+ <height>16777215</height>
+ </size>
+ </property>
+ <property name="text">
+ <string>Text Color</string>
+ </property>
+ </widget>
+ </item>
+ <item>
+ <widget class="QPushButton" name="pushButton_textColor">
+ <property name="sizePolicy">
+ <sizepolicy hsizetype="Preferred" vsizetype="Fixed">
+ <horstretch>0</horstretch>
+ <verstretch>0</verstretch>
+ </sizepolicy>
+ </property>
+ <property name="maximumSize">
+ <size>
+ <width>32</width>
+ <height>32</height>
+ </size>
+ </property>
+ <property name="text">
+ <string/>
+ </property>
+ <property name="MaximumSize" stdset="0">
+ <size>
+ <width>32</width>
+ <height>32</height>
+ </size>
+ </property>
+ </widget>
+ </item>
+ <item>
+ <spacer name="horizontalSpacer_8">
+ <property name="orientation">
+ <enum>Qt::Horizontal</enum>
+ </property>
+ <property name="sizeType">
+ <enum>QSizePolicy::Fixed</enum>
+ </property>
+ <property name="sizeHint" stdset="0">
+ <size>
+ <width>5</width>
+ <height>20</height>
+ </size>
+ </property>
+ </spacer>
+ </item>
+ <item>
+ <widget class="QLabel" name="label_fontSize">
+ <property name="sizePolicy">
+ <sizepolicy hsizetype="Fixed" vsizetype="Preferred">
+ <horstretch>0</horstretch>
+ <verstretch>0</verstretch>
+ </sizepolicy>
+ </property>
+ <property name="text">
+ <string>Font Size</string>
+ </property>
+ </widget>
+ </item>
+ <item>
+ <widget class="QSpinBox" name="spinBox_fontSize">
+ <property name="sizePolicy">
+ <sizepolicy hsizetype="Preferred" vsizetype="Fixed">
+ <horstretch>0</horstretch>
+ <verstretch>0</verstretch>
+ </sizepolicy>
+ </property>
+ <property name="suffix">
+ <string/>
+ </property>
+ <property name="prefix">
+ <string/>
+ </property>
+ <property name="minimum">
+ <number>1</number>
+ </property>
+ <property name="maximum">
+ <number>500</number>
+ </property>
+ </widget>
+ </item>
+ <item>
+ <spacer name="horizontalSpacer_7">
+ <property name="orientation">
+ <enum>Qt::Horizontal</enum>
+ </property>
+ <property name="sizeType">
+ <enum>QSizePolicy::Fixed</enum>
+ </property>
+ <property name="sizeHint" stdset="0">
+ <size>
+ <width>5</width>
+ <height>20</height>
+ </size>
+ </property>
+ </spacer>
+ </item>
+ <item>
+ <widget class="QLabel" name="label_3">
+ <property name="sizePolicy">
+ <sizepolicy hsizetype="Fixed" vsizetype="Preferred">
+ <horstretch>0</horstretch>
+ <verstretch>0</verstretch>
+ </sizepolicy>
+ </property>
+ <property name="text">
+ <string>Font Style</string>
+ </property>
+ </widget>
+ </item>
+ <item>
+ <widget class="QComboBox" name="comboBox_fontStyle">
+ <item>
+ <property name="text">
+ <string>Normal</string>
+ </property>
+ </item>
+ <item>
+ <property name="text">
+ <string>Semi-Bold</string>
+ </property>
+ </item>
+ <item>
+ <property name="text">
+ <string>Bold</string>
+ </property>
+ </item>
+ <item>
+ <property name="text">
+ <string>Italic</string>
+ </property>
+ </item>
+ <item>
+ <property name="text">
+ <string>Bold Italic</string>
+ </property>
+ </item>
+ <item>
+ <property name="text">
+ <string>Faux Italic</string>
+ </property>
+ </item>
+ <item>
+ <property name="text">
+ <string>Small Caps</string>
+ </property>
+ </item>
+ </widget>
+ </item>
+ </layout>
+ </item>
+ <item>
+ <layout class="QHBoxLayout" name="horizontalLayout_12">
+ <item>
+ <widget class="QLineEdit" name="lineEdit_textColor">
+ <property name="sizePolicy">
+ <sizepolicy hsizetype="Minimum" vsizetype="Fixed">
+ <horstretch>0</horstretch>
+ <verstretch>0</verstretch>
+ </sizepolicy>
+ </property>
+ <property name="maximumSize">
+ <size>
+ <width>0</width>
+ <height>16777215</height>
+ </size>
+ </property>
+ <property name="focusPolicy">
+ <enum>Qt::NoFocus</enum>
+ </property>
+ <property name="text">
+ <string>255,255,255</string>
+ </property>
+ </widget>
+ </item>
+ <item>
+ <widget class="QLabel" name="label_2">
+ <property name="sizePolicy">
+ <sizepolicy hsizetype="Fixed" vsizetype="Preferred">
+ <horstretch>0</horstretch>
+ <verstretch>0</verstretch>
+ </sizepolicy>
+ </property>
+ <property name="text">
+ <string>Stroke</string>
+ </property>
+ </widget>
+ </item>
+ <item>
+ <widget class="QSpinBox" name="spinBox_stroke">
+ <property name="sizePolicy">
+ <sizepolicy hsizetype="Preferred" vsizetype="Fixed">
+ <horstretch>0</horstretch>
+ <verstretch>0</verstretch>
+ </sizepolicy>
+ </property>
+ <property name="suffix">
+ <string>px</string>
+ </property>
+ </widget>
+ </item>
+ <item>
+ <widget class="QLabel" name="label_5">
+ <property name="sizePolicy">
+ <sizepolicy hsizetype="Fixed" vsizetype="Preferred">
+ <horstretch>0</horstretch>
+ <verstretch>0</verstretch>
+ </sizepolicy>
+ </property>
+ <property name="text">
+ <string>Stroke Color</string>
+ </property>
+ </widget>
+ </item>
+ <item>
+ <widget class="QLineEdit" name="lineEdit_strokeColor">
+ <property name="sizePolicy">
+ <sizepolicy hsizetype="Minimum" vsizetype="Fixed">
+ <horstretch>0</horstretch>
+ <verstretch>0</verstretch>
+ </sizepolicy>
+ </property>
+ <property name="maximumSize">
+ <size>
+ <width>0</width>
+ <height>16777215</height>
+ </size>
+ </property>
+ <property name="focusPolicy">
+ <enum>Qt::NoFocus</enum>
+ </property>
+ <property name="text">
+ <string>0,0,0</string>
+ </property>
+ </widget>
+ </item>
+ <item>
+ <widget class="QPushButton" name="pushButton_strokeColor">
+ <property name="sizePolicy">
+ <sizepolicy hsizetype="Preferred" vsizetype="Fixed">
+ <horstretch>0</horstretch>
+ <verstretch>0</verstretch>
+ </sizepolicy>
+ </property>
+ <property name="maximumSize">
+ <size>
+ <width>32</width>
+ <height>32</height>
+ </size>
+ </property>
+ <property name="text">
+ <string/>
+ </property>
+ <property name="MaximumSize" stdset="0">
+ <size>
+ <width>32</width>
+ <height>32</height>
+ </size>
+ </property>
+ </widget>
+ </item>
+ <item>
+ <spacer name="horizontalSpacer">
+ <property name="orientation">
+ <enum>Qt::Horizontal</enum>
+ </property>
+ <property name="sizeHint" stdset="0">
+ <size>
+ <width>40</width>
+ <height>20</height>
+ </size>
+ </property>
+ </spacer>
+ </item>
+ </layout>
+ </item>
+ <item>
+ <layout class="QHBoxLayout" name="horizontalLayout_2">
+ <item>
+ <widget class="QCheckBox" name="checkBox_shadow">
+ <property name="sizePolicy">
+ <sizepolicy hsizetype="Preferred" vsizetype="Fixed">
+ <horstretch>0</horstretch>
+ <verstretch>0</verstretch>
+ </sizepolicy>
+ </property>
+ <property name="text">
+ <string>Shadow</string>
+ </property>
+ </widget>
+ </item>
+ <item>
+ <widget class="QLabel" name="label_shadX">
+ <property name="sizePolicy">
+ <sizepolicy hsizetype="Fixed" vsizetype="Preferred">
+ <horstretch>0</horstretch>
+ <verstretch>0</verstretch>
+ </sizepolicy>
+ </property>
+ <property name="text">
+ <string>Shadow Offset</string>
+ </property>
+ </widget>
+ </item>
+ <item>
+ <widget class="QSpinBox" name="spinBox_shadX">
+ <property name="sizePolicy">
+ <sizepolicy hsizetype="Fixed" vsizetype="Fixed">
+ <horstretch>0</horstretch>
+ <verstretch>0</verstretch>
+ </sizepolicy>
+ </property>
+ <property name="minimum">
+ <number>-1000</number>
+ </property>
+ <property name="maximum">
+ <number>1000</number>
+ </property>
+ <property name="value">
+ <number>-4</number>
+ </property>
+ </widget>
+ </item>
+ <item>
+ <widget class="QSpinBox" name="spinBox_shadY">
+ <property name="sizePolicy">
+ <sizepolicy hsizetype="Fixed" vsizetype="Fixed">
+ <horstretch>0</horstretch>
+ <verstretch>0</verstretch>
+ </sizepolicy>
+ </property>
+ <property name="minimum">
+ <number>-1000</number>
+ </property>
+ <property name="maximum">
+ <number>1000</number>
+ </property>
+ <property name="value">
+ <number>8</number>
+ </property>
+ </widget>
+ </item>
+ <item>
+ <widget class="QLabel" name="label_shadBlur">
+ <property name="sizePolicy">
+ <sizepolicy hsizetype="Fixed" vsizetype="Preferred">
+ <horstretch>0</horstretch>
+ <verstretch>0</verstretch>
+ </sizepolicy>
+ </property>
+ <property name="text">
+ <string>Shadow Blur</string>
+ </property>
+ </widget>
+ </item>
+ <item>
+ <widget class="QDoubleSpinBox" name="spinBox_shadBlur">
+ <property name="sizePolicy">
+ <sizepolicy hsizetype="Preferred" vsizetype="Fixed">
+ <horstretch>0</horstretch>
+ <verstretch>0</verstretch>
+ </sizepolicy>
+ </property>
+ <property name="maximum">
+ <double>99.000000000000000</double>
+ </property>
+ <property name="singleStep">
+ <double>0.100000000000000</double>
+ </property>
+ <property name="value">
+ <double>5.000000000000000</double>
+ </property>
+ </widget>
+ </item>
+ <item>
+ <spacer name="horizontalSpacer_3">
+ <property name="orientation">
+ <enum>Qt::Horizontal</enum>
+ </property>
+ <property name="sizeType">
+ <enum>QSizePolicy::Minimum</enum>
+ </property>
+ <property name="sizeHint" stdset="0">
+ <size>
+ <width>40</width>
+ <height>20</height>
+ </size>
+ </property>
+ </spacer>
+ </item>
+ </layout>
+ </item>
+ </layout>
+ </item>
+ <item>
+ <spacer name="verticalSpacer">
+ <property name="orientation">
+ <enum>Qt::Vertical</enum>
+ </property>
+ <property name="sizeHint" stdset="0">
+ <size>
+ <width>20</width>
+ <height>40</height>
+ </size>
+ </property>
+ </spacer>
+ </item>
+ </layout>
+ </widget>
+ <resources/>
+ <connections/>
+</ui>
diff --git a/src/avp/components/video.py b/src/avp/components/video.py
new file mode 100644
index 0000000..65a05af
--- /dev/null
+++ b/src/avp/components/video.py
@@ -0,0 +1,254 @@
+from PIL import Image
+from PyQt6 import QtGui, QtCore, QtWidgets
+import os
+import math
+import subprocess
+import logging
+
+from ..component import Component
+from ..toolkit.frame import BlankFrame, scale
+from ..toolkit.ffmpeg import openPipe, closePipe, testAudioStream, FfmpegVideo
+from ..toolkit import checkOutput
+
+
+log = logging.getLogger("AVP.Components.Video")
+
+
+class Component(Component):
+ name = "Video"
+ version = "1.0.0"
+
+ def widget(self, *args):
+ self.videoPath = ""
+ self.badAudio = False
+ self.x = 0
+ self.y = 0
+ self.loopVideo = False
+ super().widget(*args)
+ self._image = BlankFrame(self.width, self.height)
+ self.page.pushButton_video.clicked.connect(self.pickVideo)
+ self.trackWidgets(
+ {
+ "videoPath": self.page.lineEdit_video,
+ "loopVideo": self.page.checkBox_loop,
+ "useAudio": self.page.checkBox_useAudio,
+ "distort": self.page.checkBox_distort,
+ "scale": self.page.spinBox_scale,
+ "volume": self.page.spinBox_volume,
+ "xPosition": self.page.spinBox_x,
+ "yPosition": self.page.spinBox_y,
+ },
+ presetNames={
+ "videoPath": "video",
+ "loopVideo": "loop",
+ "xPosition": "x",
+ "yPosition": "y",
+ },
+ relativeWidgets=[
+ "xPosition",
+ "yPosition",
+ ],
+ )
+
+ def update(self):
+ if self.page.checkBox_useAudio.isChecked():
+ self.page.label_volume.setEnabled(True)
+ self.page.spinBox_volume.setEnabled(True)
+ else:
+ self.page.label_volume.setEnabled(False)
+ self.page.spinBox_volume.setEnabled(False)
+
+ def previewRender(self):
+ self.updateChunksize()
+ frame = self.getPreviewFrame(self.width, self.height)
+ if not frame:
+ return BlankFrame(self.width, self.height)
+ else:
+ return frame
+
+ def properties(self):
+ props = []
+ outputFile = None
+ if hasattr(self.parent, "lineEdit_outputFile"):
+ # check only happens in GUI mode
+ outputFile = self.parent.lineEdit_outputFile.text()
+
+ if not self.videoPath:
+ self.lockError("There is no video selected.")
+ elif not os.path.exists(self.videoPath):
+ self.lockError("The video selected does not exist!")
+ elif outputFile and os.path.realpath(self.videoPath) == os.path.realpath(
+ outputFile
+ ):
+ self.lockError("Input and output paths match.")
+
+ if self.useAudio:
+ props.append("audio")
+ if not testAudioStream(self.videoPath) and self.error() is None:
+ self.lockError("Could not identify an audio stream in this video.")
+
+ return props
+
+ def audio(self):
+ params = {}
+ if self.volume != 1.0:
+ params["volume"] = "=%s:replaygain_noclip=0" % str(self.volume)
+ return (self.videoPath, params)
+
+ def preFrameRender(self, **kwargs):
+ super().preFrameRender(**kwargs)
+ self.updateChunksize()
+ self.video = (
+ FfmpegVideo(
+ inputPath=self.videoPath,
+ filter_=self.makeFfmpegFilter(),
+ width=self.width,
+ height=self.height,
+ chunkSize=self.chunkSize,
+ frameRate=int(self.settings.value("outputFrameRate")),
+ parent=self.parent,
+ loopVideo=self.loopVideo,
+ component=self,
+ )
+ if os.path.exists(self.videoPath)
+ else None
+ )
+
+ def frameRender(self, frameNo):
+ if FfmpegVideo.threadError is not None:
+ raise FfmpegVideo.threadError
+ return self.finalizeFrame(self.video.frame(frameNo))
+
+ def postFrameRender(self):
+ closePipe(self.video.pipe)
+
+ def pickVideo(self):
+ imgDir = self.settings.value("componentDir", os.path.expanduser("~"))
+ filename, _ = QtWidgets.QFileDialog.getOpenFileName(
+ self.page,
+ "Choose Video",
+ imgDir,
+ "Video Files (%s)" % " ".join(self.core.videoFormats),
+ )
+ if filename:
+ self.settings.setValue("componentDir", os.path.dirname(filename))
+ self.mergeUndo = False
+ self.page.lineEdit_video.setText(filename)
+ self.mergeUndo = True
+
+ def getPreviewFrame(self, width, height):
+ if not self.videoPath or not os.path.exists(self.videoPath):
+ return
+
+ command = [
+ self.core.FFMPEG_BIN,
+ "-thread_queue_size",
+ "512",
+ "-i",
+ self.videoPath,
+ "-f",
+ "image2pipe",
+ "-pix_fmt",
+ "rgba",
+ ]
+ command.extend(self.makeFfmpegFilter())
+ command.extend(
+ [
+ "-codec:v",
+ "rawvideo",
+ "-",
+ "-ss",
+ "90",
+ "-frames:v",
+ "1",
+ ]
+ )
+
+ if self.core.logEnabled:
+ logFilename = os.path.join(
+ self.core.logDir, "preview_%s.log" % str(self.compPos)
+ )
+ log.debug("Creating ffmpeg process (log at %s)" % logFilename)
+ with open(logFilename, "w") as logf:
+ logf.write(" ".join(command) + "\n\n")
+ with open(logFilename, "a") as logf:
+ pipe = openPipe(
+ command,
+ stdin=subprocess.DEVNULL,
+ stdout=subprocess.PIPE,
+ stderr=logf,
+ bufsize=10**8,
+ )
+ else:
+ pipe = openPipe(
+ command,
+ stdin=subprocess.DEVNULL,
+ stdout=subprocess.PIPE,
+ stderr=subprocess.DEVNULL,
+ bufsize=10**8,
+ )
+
+ byteFrame = pipe.stdout.read(self.chunkSize)
+ closePipe(pipe)
+
+ frame = self.finalizeFrame(byteFrame)
+ return frame
+
+ def makeFfmpegFilter(self):
+ return [
+ "-filter_complex",
+ "[0:v] scale=%s:%s" % scale(self.scale, self.width, self.height, str),
+ ]
+
+ def updateChunksize(self):
+ if self.scale != 100 and not self.distort:
+ width, height = scale(self.scale, self.width, self.height, int)
+ else:
+ width, height = self.width, self.height
+ self.chunkSize = 4 * width * height
+
+ def command(self, arg):
+ if "=" in arg:
+ key, arg = arg.split("=", 1)
+ if key == "path" and os.path.exists(arg):
+ if "*%s" % os.path.splitext(arg)[1] in self.core.videoFormats:
+ self.page.lineEdit_video.setText(arg)
+ self.page.spinBox_scale.setValue(100)
+ self.page.checkBox_loop.setChecked(True)
+ return
+ else:
+ print("Not a supported video format")
+ quit(1)
+ elif arg == "audio":
+ if not self.page.lineEdit_video.text():
+ print("'audio' option must follow a video selection")
+ quit(1)
+ self.page.checkBox_useAudio.setChecked(True)
+ return
+ super().command(arg)
+
+ def commandHelp(self):
+ print("Load a video:\n path=/filepath/to/video.mp4")
+ print("Using audio:\n path=/filepath/to/video.mp4 audio")
+
+ def finalizeFrame(self, imageData):
+ try:
+ if self.distort:
+ image = Image.frombytes("RGBA", (self.width, self.height), imageData)
+ else:
+ image = Image.frombytes(
+ "RGBA",
+ scale(self.scale, self.width, self.height, int),
+ imageData,
+ )
+ self._image = image
+ except ValueError:
+ # use last good frame
+ image = self._image
+
+ if self.scale != 100 or self.xPosition != 0 or self.yPosition != 0:
+ frame = BlankFrame(self.width, self.height)
+ frame.paste(image, box=(self.xPosition, self.yPosition))
+ else:
+ frame = image
+ return frame
diff --git a/src/avp/components/video.ui b/src/avp/components/video.ui
new file mode 100644
index 0000000..08d15d3
--- /dev/null
+++ b/src/avp/components/video.ui
@@ -0,0 +1,328 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<ui version="4.0">
+ <class>Form</class>
+ <widget class="QWidget" name="Form">
+ <property name="geometry">
+ <rect>
+ <x>0</x>
+ <y>0</y>
+ <width>586</width>
+ <height>197</height>
+ </rect>
+ </property>
+ <property name="sizePolicy">
+ <sizepolicy hsizetype="Preferred" vsizetype="Preferred">
+ <horstretch>0</horstretch>
+ <verstretch>0</verstretch>
+ </sizepolicy>
+ </property>
+ <property name="minimumSize">
+ <size>
+ <width>0</width>
+ <height>197</height>
+ </size>
+ </property>
+ <property name="windowTitle">
+ <string>Form</string>
+ </property>
+ <layout class="QVBoxLayout" name="verticalLayout_2">
+ <item>
+ <layout class="QVBoxLayout" name="verticalLayout">
+ <property name="leftMargin">
+ <number>4</number>
+ </property>
+ <item>
+ <layout class="QHBoxLayout" name="horizontalLayout_8">
+ <item>
+ <widget class="QLabel" name="label_textColor">
+ <property name="sizePolicy">
+ <sizepolicy hsizetype="Fixed" vsizetype="Preferred">
+ <horstretch>0</horstretch>
+ <verstretch>0</verstretch>
+ </sizepolicy>
+ </property>
+ <property name="minimumSize">
+ <size>
+ <width>31</width>
+ <height>0</height>
+ </size>
+ </property>
+ <property name="text">
+ <string>Video</string>
+ </property>
+ </widget>
+ </item>
+ <item>
+ <widget class="QLineEdit" name="lineEdit_video">
+ <property name="minimumSize">
+ <size>
+ <width>1</width>
+ <height>0</height>
+ </size>
+ </property>
+ </widget>
+ </item>
+ <item>
+ <widget class="QPushButton" name="pushButton_video">
+ <property name="sizePolicy">
+ <sizepolicy hsizetype="Fixed" vsizetype="Fixed">
+ <horstretch>0</horstretch>
+ <verstretch>0</verstretch>
+ </sizepolicy>
+ </property>
+ <property name="minimumSize">
+ <size>
+ <width>1</width>
+ <height>0</height>
+ </size>
+ </property>
+ <property name="maximumSize">
+ <size>
+ <width>32</width>
+ <height>32</height>
+ </size>
+ </property>
+ <property name="text">
+ <string>...</string>
+ </property>
+ <property name="MaximumSize" stdset="0">
+ <size>
+ <width>32</width>
+ <height>32</height>
+ </size>
+ </property>
+ </widget>
+ </item>
+ <item>
+ <spacer name="horizontalSpacer_9">
+ <property name="orientation">
+ <enum>Qt::Horizontal</enum>
+ </property>
+ <property name="sizeType">
+ <enum>QSizePolicy::Fixed</enum>
+ </property>
+ <property name="sizeHint" stdset="0">
+ <size>
+ <width>5</width>
+ <height>20</height>
+ </size>
+ </property>
+ </spacer>
+ </item>
+ <item>
+ <widget class="QLabel" name="label_xTitleAlign">
+ <property name="sizePolicy">
+ <sizepolicy hsizetype="Fixed" vsizetype="Preferred">
+ <horstretch>0</horstretch>
+ <verstretch>0</verstretch>
+ </sizepolicy>
+ </property>
+ <property name="text">
+ <string>X</string>
+ </property>
+ </widget>
+ </item>
+ <item>
+ <widget class="QSpinBox" name="spinBox_x">
+ <property name="sizePolicy">
+ <sizepolicy hsizetype="Fixed" vsizetype="Fixed">
+ <horstretch>0</horstretch>
+ <verstretch>0</verstretch>
+ </sizepolicy>
+ </property>
+ <property name="maximumSize">
+ <size>
+ <width>80</width>
+ <height>16777215</height>
+ </size>
+ </property>
+ <property name="minimum">
+ <number>-10000</number>
+ </property>
+ <property name="maximum">
+ <number>10000</number>
+ </property>
+ </widget>
+ </item>
+ <item>
+ <widget class="QLabel" name="label_yTitleAlign">
+ <property name="sizePolicy">
+ <sizepolicy hsizetype="Fixed" vsizetype="Preferred">
+ <horstretch>0</horstretch>
+ <verstretch>0</verstretch>
+ </sizepolicy>
+ </property>
+ <property name="text">
+ <string>Y</string>
+ </property>
+ </widget>
+ </item>
+ <item>
+ <widget class="QSpinBox" name="spinBox_y">
+ <property name="sizePolicy">
+ <sizepolicy hsizetype="Fixed" vsizetype="Fixed">
+ <horstretch>0</horstretch>
+ <verstretch>0</verstretch>
+ </sizepolicy>
+ </property>
+ <property name="maximumSize">
+ <size>
+ <width>80</width>
+ <height>16777215</height>
+ </size>
+ </property>
+ <property name="baseSize">
+ <size>
+ <width>0</width>
+ <height>0</height>
+ </size>
+ </property>
+ <property name="minimum">
+ <number>-10000</number>
+ </property>
+ <property name="maximum">
+ <number>10000</number>
+ </property>
+ <property name="value">
+ <number>0</number>
+ </property>
+ </widget>
+ </item>
+ </layout>
+ </item>
+ </layout>
+ </item>
+ <item>
+ <layout class="QHBoxLayout" name="horizontalLayout_9">
+ <item>
+ <widget class="QCheckBox" name="checkBox_loop">
+ <property name="text">
+ <string>Loop</string>
+ </property>
+ </widget>
+ </item>
+ <item>
+ <spacer name="horizontalSpacer">
+ <property name="orientation">
+ <enum>Qt::Horizontal</enum>
+ </property>
+ <property name="sizeHint" stdset="0">
+ <size>
+ <width>40</width>
+ <height>20</height>
+ </size>
+ </property>
+ </spacer>
+ </item>
+ <item>
+ <widget class="QCheckBox" name="checkBox_distort">
+ <property name="text">
+ <string>Distort by scale</string>
+ </property>
+ </widget>
+ </item>
+ <item>
+ <widget class="QLabel" name="label">
+ <property name="text">
+ <string>Scale</string>
+ </property>
+ <property name="alignment">
+ <set>Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter</set>
+ </property>
+ </widget>
+ </item>
+ <item>
+ <widget class="QSpinBox" name="spinBox_scale">
+ <property name="buttonSymbols">
+ <enum>QAbstractSpinBox::UpDownArrows</enum>
+ </property>
+ <property name="suffix">
+ <string>%</string>
+ </property>
+ <property name="minimum">
+ <number>10</number>
+ </property>
+ <property name="maximum">
+ <number>400</number>
+ </property>
+ <property name="value">
+ <number>100</number>
+ </property>
+ </widget>
+ </item>
+ </layout>
+ </item>
+ <item>
+ <layout class="QHBoxLayout" name="horizontalLayout_10">
+ <item>
+ <widget class="QCheckBox" name="checkBox_useAudio">
+ <property name="text">
+ <string>Use Audio</string>
+ </property>
+ </widget>
+ </item>
+ <item>
+ <widget class="QLabel" name="label_volume">
+ <property name="text">
+ <string>Volume</string>
+ </property>
+ </widget>
+ </item>
+ <item>
+ <widget class="QDoubleSpinBox" name="spinBox_volume">
+ <property name="sizePolicy">
+ <sizepolicy hsizetype="Fixed" vsizetype="Fixed">
+ <horstretch>0</horstretch>
+ <verstretch>0</verstretch>
+ </sizepolicy>
+ </property>
+ <property name="suffix">
+ <string>x</string>
+ </property>
+ <property name="minimum">
+ <double>0.000000000000000</double>
+ </property>
+ <property name="maximum">
+ <double>10.000000000000000</double>
+ </property>
+ <property name="singleStep">
+ <double>0.100000000000000</double>
+ </property>
+ <property name="value">
+ <double>1.000000000000000</double>
+ </property>
+ </widget>
+ </item>
+ <item>
+ <spacer name="horizontalSpacer_2">
+ <property name="orientation">
+ <enum>Qt::Horizontal</enum>
+ </property>
+ <property name="sizeHint" stdset="0">
+ <size>
+ <width>40</width>
+ <height>20</height>
+ </size>
+ </property>
+ </spacer>
+ </item>
+ </layout>
+ </item>
+ <item>
+ <spacer name="verticalSpacer">
+ <property name="orientation">
+ <enum>Qt::Vertical</enum>
+ </property>
+ <property name="sizeHint" stdset="0">
+ <size>
+ <width>20</width>
+ <height>40</height>
+ </size>
+ </property>
+ </spacer>
+ </item>
+ </layout>
+ </widget>
+ <resources/>
+ <connections/>
+</ui>
diff --git a/src/avp/components/waveform.py b/src/avp/components/waveform.py
new file mode 100644
index 0000000..7dc0b99
--- /dev/null
+++ b/src/avp/components/waveform.py
@@ -0,0 +1,230 @@
+from PIL import Image
+from PyQt6 import QtGui, QtCore, QtWidgets
+from PyQt6.QtGui import QColor
+import os
+import math
+import subprocess
+import logging
+
+from ..component import Component
+from ..toolkit.frame import BlankFrame, scale
+from ..toolkit import checkOutput
+from ..toolkit.ffmpeg import (
+ openPipe,
+ closePipe,
+ getAudioDuration,
+ FfmpegVideo,
+ exampleSound,
+)
+
+
+log = logging.getLogger("AVP.Components.Waveform")
+
+
+class Component(Component):
+ name = "Waveform"
+ version = "1.0.0"
+
+ def widget(self, *args):
+ super().widget(*args)
+ self._image = BlankFrame(self.width, self.height)
+
+ self.page.lineEdit_color.setText("255,255,255")
+
+ if hasattr(self.parent, "lineEdit_audioFile"):
+ self.parent.lineEdit_audioFile.textChanged.connect(self.update)
+
+ self.trackWidgets(
+ {
+ "color": self.page.lineEdit_color,
+ "mode": self.page.comboBox_mode,
+ "amplitude": self.page.comboBox_amplitude,
+ "x": self.page.spinBox_x,
+ "y": self.page.spinBox_y,
+ "mirror": self.page.checkBox_mirror,
+ "scale": self.page.spinBox_scale,
+ "opacity": self.page.spinBox_opacity,
+ "compress": self.page.checkBox_compress,
+ "mono": self.page.checkBox_mono,
+ },
+ colorWidgets={
+ "color": self.page.pushButton_color,
+ },
+ relativeWidgets=[
+ "x",
+ "y",
+ ],
+ )
+
+ def previewRender(self):
+ self.updateChunksize()
+ frame = self.getPreviewFrame(self.width, self.height)
+ if not frame:
+ return BlankFrame(self.width, self.height)
+ else:
+ return frame
+
+ def preFrameRender(self, **kwargs):
+ super().preFrameRender(**kwargs)
+ self.updateChunksize()
+ w, h = scale(self.scale, self.width, self.height, str)
+ self.video = FfmpegVideo(
+ inputPath=self.audioFile,
+ filter_=self.makeFfmpegFilter(),
+ width=w,
+ height=h,
+ chunkSize=self.chunkSize,
+ frameRate=int(self.settings.value("outputFrameRate")),
+ parent=self.parent,
+ component=self,
+ debug=True,
+ )
+
+ def frameRender(self, frameNo):
+ if FfmpegVideo.threadError is not None:
+ raise FfmpegVideo.threadError
+ return self.finalizeFrame(self.video.frame(frameNo))
+
+ def postFrameRender(self):
+ closePipe(self.video.pipe)
+
+ def getPreviewFrame(self, width, height):
+ genericPreview = self.settings.value("pref_genericPreview")
+ startPt = 0
+ if not genericPreview:
+ inputFile = self.parent.lineEdit_audioFile.text()
+ if not inputFile or not os.path.exists(inputFile):
+ return
+ duration = getAudioDuration(inputFile)
+ if not duration:
+ return
+ startPt = duration / 3
+ if startPt + 3 > duration:
+ startPt += startPt - 3
+
+ command = [
+ self.core.FFMPEG_BIN,
+ "-thread_queue_size",
+ "512",
+ "-r",
+ str(self.settings.value("outputFrameRate")),
+ "-ss",
+ "{0:.3f}".format(startPt),
+ "-i",
+ self.core.junkStream if genericPreview else inputFile,
+ "-f",
+ "image2pipe",
+ "-pix_fmt",
+ "rgba",
+ ]
+ command.extend(self.makeFfmpegFilter(preview=True, startPt=startPt))
+ command.extend(
+ [
+ "-an",
+ "-s:v",
+ "%sx%s" % scale(self.scale, self.width, self.height, str),
+ "-codec:v",
+ "rawvideo",
+ "-",
+ "-frames:v",
+ "1",
+ ]
+ )
+ if self.core.logEnabled:
+ logFilename = os.path.join(
+ self.core.logDir, "preview_%s.log" % str(self.compPos)
+ )
+ log.debug("Creating ffmpeg log at %s", logFilename)
+ with open(logFilename, "w") as logf:
+ logf.write(" ".join(command) + "\n\n")
+ with open(logFilename, "a") as logf:
+ pipe = openPipe(
+ command,
+ stdin=subprocess.DEVNULL,
+ stdout=subprocess.PIPE,
+ stderr=logf,
+ bufsize=10**8,
+ )
+ else:
+ pipe = openPipe(
+ command,
+ stdin=subprocess.DEVNULL,
+ stdout=subprocess.PIPE,
+ stderr=subprocess.DEVNULL,
+ bufsize=10**8,
+ )
+ byteFrame = pipe.stdout.read(self.chunkSize)
+ closePipe(pipe)
+
+ frame = self.finalizeFrame(byteFrame)
+ return frame
+
+ def makeFfmpegFilter(self, preview=False, startPt=0):
+ w, h = scale(self.scale, self.width, self.height, str)
+ if self.amplitude == 0:
+ amplitude = "lin"
+ elif self.amplitude == 1:
+ amplitude = "log"
+ elif self.amplitude == 2:
+ amplitude = "sqrt"
+ elif self.amplitude == 3:
+ amplitude = "cbrt"
+ hexcolor = QColor(*self.color).name()
+ opacity = "{0:.1f}".format(self.opacity / 100)
+ genericPreview = self.settings.value("pref_genericPreview")
+ if self.mode < 3:
+ filter_ = (
+ "showwaves="
+ f'r={str(self.settings.value("outputFrameRate"))}:'
+ f's={self.settings.value("outputWidth")}x{self.settings.value("outputHeight")}:'
+ f'mode={self.page.comboBox_mode.currentText().lower() if self.mode != 3 else "p2p"}:'
+ f"colors={hexcolor}@{opacity}:scale={amplitude}"
+ )
+ elif self.mode > 2:
+ filter_ = (
+ f'showfreqs=s={str(self.settings.value("outputWidth"))}x{str(self.settings.value("outputHeight"))}:'
+ f'mode={"line" if self.mode == 4 else "bar"}:'
+ f"colors={hexcolor}@{opacity}"
+ f":ascale={amplitude}:fscale={'log' if self.mono else 'lin'}"
+ )
+
+ baselineHeight = int(self.height * (4 / 1080))
+ return [
+ "-filter_complex",
+ f"{exampleSound('wave', extra='') if preview and genericPreview else '[0:a] '}"
+ f"{'compand=gain=4,' if self.compress else ''}"
+ f"{'aformat=channel_layouts=mono,' if self.mono and self.mode < 3 else ''}"
+ f"{filter_}"
+ f"{', drawbox=x=(iw-w)/2:y=(ih-h)/2:w=iw:h=%s:color=%s@%s' % (baselineHeight, hexcolor, opacity) if self.mode < 2 else ''}"
+ f"{', hflip' if self.mirror else''}"
+ " [v1]; "
+ "[v1] scale=%s:%s%s [v]"
+ % (
+ w,
+ h,
+ ", trim=duration=%s" % "{0:.3f}".format(startPt + 3) if preview else "",
+ ),
+ "-map",
+ "[v]",
+ ]
+
+ def updateChunksize(self):
+ width, height = scale(self.scale, self.width, self.height, int)
+ self.chunkSize = 4 * width * height
+
+ def finalizeFrame(self, imageData):
+ try:
+ image = Image.frombytes(
+ "RGBA",
+ scale(self.scale, self.width, self.height, int),
+ imageData,
+ )
+ self._image = image
+ except ValueError:
+ image = self._image
+ if self.scale != 100 or self.x != 0 or self.y != 0:
+ frame = BlankFrame(self.width, self.height)
+ frame.paste(image, box=(self.x, self.y))
+ else:
+ frame = image
+ return frame
diff --git a/src/avp/components/waveform.ui b/src/avp/components/waveform.ui
new file mode 100644
index 0000000..5473f33
--- /dev/null
+++ b/src/avp/components/waveform.ui
@@ -0,0 +1,383 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<ui version="4.0">
+ <class>Form</class>
+ <widget class="QWidget" name="Form">
+ <property name="geometry">
+ <rect>
+ <x>0</x>
+ <y>0</y>
+ <width>586</width>
+ <height>197</height>
+ </rect>
+ </property>
+ <property name="sizePolicy">
+ <sizepolicy hsizetype="Preferred" vsizetype="Preferred">
+ <horstretch>0</horstretch>
+ <verstretch>0</verstretch>
+ </sizepolicy>
+ </property>
+ <property name="minimumSize">
+ <size>
+ <width>0</width>
+ <height>197</height>
+ </size>
+ </property>
+ <property name="windowTitle">
+ <string>Form</string>
+ </property>
+ <layout class="QVBoxLayout" name="verticalLayout_2">
+ <item>
+ <layout class="QVBoxLayout" name="verticalLayout">
+ <property name="leftMargin">
+ <number>4</number>
+ </property>
+ <item>
+ <layout class="QHBoxLayout" name="horizontalLayout_8">
+ <item>
+ <widget class="QLabel" name="label_textColor">
+ <property name="sizePolicy">
+ <sizepolicy hsizetype="Fixed" vsizetype="Preferred">
+ <horstretch>0</horstretch>
+ <verstretch>0</verstretch>
+ </sizepolicy>
+ </property>
+ <property name="minimumSize">
+ <size>
+ <width>31</width>
+ <height>0</height>
+ </size>
+ </property>
+ <property name="text">
+ <string>Mode</string>
+ </property>
+ </widget>
+ </item>
+ <item>
+ <widget class="QComboBox" name="comboBox_mode">
+ <item>
+ <property name="text">
+ <string>Cline</string>
+ </property>
+ </item>
+ <item>
+ <property name="text">
+ <string>Line</string>
+ </property>
+ </item>
+ <item>
+ <property name="text">
+ <string>Point</string>
+ </property>
+ </item>
+ <item>
+ <property name="text">
+ <string>Frequency Bar</string>
+ </property>
+ </item>
+ <item>
+ <property name="text">
+ <string>Frequency Line</string>
+ </property>
+ </item>
+ </widget>
+ </item>
+ <item>
+ <spacer name="horizontalSpacer_9">
+ <property name="orientation">
+ <enum>Qt::Horizontal</enum>
+ </property>
+ <property name="sizeType">
+ <enum>QSizePolicy::Fixed</enum>
+ </property>
+ <property name="sizeHint" stdset="0">
+ <size>
+ <width>5</width>
+ <height>20</height>
+ </size>
+ </property>
+ </spacer>
+ </item>
+ <item>
+ <widget class="QLabel" name="label_xTitleAlign">
+ <property name="sizePolicy">
+ <sizepolicy hsizetype="Fixed" vsizetype="Preferred">
+ <horstretch>0</horstretch>
+ <verstretch>0</verstretch>
+ </sizepolicy>
+ </property>
+ <property name="text">
+ <string>X</string>
+ </property>
+ </widget>
+ </item>
+ <item>
+ <widget class="QSpinBox" name="spinBox_x">
+ <property name="sizePolicy">
+ <sizepolicy hsizetype="Fixed" vsizetype="Fixed">
+ <horstretch>0</horstretch>
+ <verstretch>0</verstretch>
+ </sizepolicy>
+ </property>
+ <property name="maximumSize">
+ <size>
+ <width>80</width>
+ <height>16777215</height>
+ </size>
+ </property>
+ <property name="minimum">
+ <number>-10000</number>
+ </property>
+ <property name="maximum">
+ <number>10000</number>
+ </property>
+ </widget>
+ </item>
+ <item>
+ <widget class="QLabel" name="label_yTitleAlign">
+ <property name="sizePolicy">
+ <sizepolicy hsizetype="Fixed" vsizetype="Preferred">
+ <horstretch>0</horstretch>
+ <verstretch>0</verstretch>
+ </sizepolicy>
+ </property>
+ <property name="text">
+ <string>Y</string>
+ </property>
+ </widget>
+ </item>
+ <item>
+ <widget class="QSpinBox" name="spinBox_y">
+ <property name="sizePolicy">
+ <sizepolicy hsizetype="Fixed" vsizetype="Fixed">
+ <horstretch>0</horstretch>
+ <verstretch>0</verstretch>
+ </sizepolicy>
+ </property>
+ <property name="maximumSize">
+ <size>
+ <width>80</width>
+ <height>16777215</height>
+ </size>
+ </property>
+ <property name="baseSize">
+ <size>
+ <width>0</width>
+ <height>0</height>
+ </size>
+ </property>
+ <property name="minimum">
+ <number>-10000</number>
+ </property>
+ <property name="maximum">
+ <number>10000</number>
+ </property>
+ <property name="value">
+ <number>0</number>
+ </property>
+ </widget>
+ </item>
+ </layout>
+ </item>
+ </layout>
+ </item>
+ <item>
+ <layout class="QHBoxLayout" name="horizontalLayout_9">
+ <item>
+ <widget class="QLabel" name="label_2">
+ <property name="text">
+ <string>Color</string>
+ </property>
+ </widget>
+ </item>
+ <item>
+ <widget class="QLineEdit" name="lineEdit_color">
+ <property name="inputMethodHints">
+ <set>Qt::ImhNone</set>
+ </property>
+ </widget>
+ </item>
+ <item>
+ <widget class="QPushButton" name="pushButton_color">
+ <property name="sizePolicy">
+ <sizepolicy hsizetype="Fixed" vsizetype="Fixed">
+ <horstretch>0</horstretch>
+ <verstretch>0</verstretch>
+ </sizepolicy>
+ </property>
+ <property name="maximumSize">
+ <size>
+ <width>32</width>
+ <height>32</height>
+ </size>
+ </property>
+ <property name="text">
+ <string/>
+ </property>
+ <property name="default">
+ <bool>false</bool>
+ </property>
+ <property name="flat">
+ <bool>false</bool>
+ </property>
+ </widget>
+ </item>
+ <item>
+ <spacer name="horizontalSpacer">
+ <property name="orientation">
+ <enum>Qt::Horizontal</enum>
+ </property>
+ <property name="sizeHint" stdset="0">
+ <size>
+ <width>40</width>
+ <height>20</height>
+ </size>
+ </property>
+ </spacer>
+ </item>
+ <item>
+ <widget class="QLabel" name="label_4">
+ <property name="text">
+ <string>Opacity</string>
+ </property>
+ <property name="alignment">
+ <set>Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter</set>
+ </property>
+ </widget>
+ </item>
+ <item>
+ <widget class="QSpinBox" name="spinBox_opacity">
+ <property name="buttonSymbols">
+ <enum>QAbstractSpinBox::UpDownArrows</enum>
+ </property>
+ <property name="suffix">
+ <string>%</string>
+ </property>
+ <property name="minimum">
+ <number>0</number>
+ </property>
+ <property name="maximum">
+ <number>100</number>
+ </property>
+ <property name="value">
+ <number>100</number>
+ </property>
+ </widget>
+ </item>
+ <item>
+ <widget class="QLabel" name="label">
+ <property name="text">
+ <string>Scale</string>
+ </property>
+ <property name="alignment">
+ <set>Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter</set>
+ </property>
+ </widget>
+ </item>
+ <item>
+ <widget class="QSpinBox" name="spinBox_scale">
+ <property name="buttonSymbols">
+ <enum>QAbstractSpinBox::UpDownArrows</enum>
+ </property>
+ <property name="suffix">
+ <string>%</string>
+ </property>
+ <property name="minimum">
+ <number>10</number>
+ </property>
+ <property name="maximum">
+ <number>400</number>
+ </property>
+ <property name="value">
+ <number>100</number>
+ </property>
+ </widget>
+ </item>
+ </layout>
+ </item>
+ <item>
+ <layout class="QHBoxLayout" name="horizontalLayout_10">
+ <item>
+ <widget class="QCheckBox" name="checkBox_compress">
+ <property name="text">
+ <string>Compress</string>
+ </property>
+ </widget>
+ </item>
+ <item>
+ <widget class="QCheckBox" name="checkBox_mono">
+ <property name="text">
+ <string>Mono</string>
+ </property>
+ </widget>
+ </item>
+ <item>
+ <widget class="QCheckBox" name="checkBox_mirror">
+ <property name="text">
+ <string>Mirror</string>
+ </property>
+ </widget>
+ </item>
+ <item>
+ <spacer name="horizontalSpacer_2">
+ <property name="orientation">
+ <enum>Qt::Horizontal</enum>
+ </property>
+ <property name="sizeHint" stdset="0">
+ <size>
+ <width>40</width>
+ <height>20</height>
+ </size>
+ </property>
+ </spacer>
+ </item>
+ <item>
+ <widget class="QLabel" name="label_3">
+ <property name="text">
+ <string>Amplitude</string>
+ </property>
+ </widget>
+ </item>
+ <item>
+ <widget class="QComboBox" name="comboBox_amplitude">
+ <item>
+ <property name="text">
+ <string>Linear</string>
+ </property>
+ </item>
+ <item>
+ <property name="text">
+ <string>Logarithmic</string>
+ </property>
+ </item>
+ <item>
+ <property name="text">
+ <string>Square root</string>
+ </property>
+ </item>
+ <item>
+ <property name="text">
+ <string>Cubic root</string>
+ </property>
+ </item>
+ </widget>
+ </item>
+ </layout>
+ </item>
+ <item>
+ <spacer name="verticalSpacer">
+ <property name="orientation">
+ <enum>Qt::Vertical</enum>
+ </property>
+ <property name="sizeHint" stdset="0">
+ <size>
+ <width>20</width>
+ <height>40</height>
+ </size>
+ </property>
+ </spacer>
+ </item>
+ </layout>
+ </widget>
+ <resources/>
+ <connections/>
+</ui>