1#!/usr/bin/env python 2 3 4############################################################################# 5## 6## Copyright (C) 2013 Riverbank Computing Limited. 7## Copyright (C) 2010 Nokia Corporation and/or its subsidiary(-ies). 8## All rights reserved. 9## 10## This file is part of the examples of PyQt. 11## 12## $QT_BEGIN_LICENSE:BSD$ 13## You may use this file under the terms of the BSD license as follows: 14## 15## "Redistribution and use in source and binary forms, with or without 16## modification, are permitted provided that the following conditions are 17## met: 18## * Redistributions of source code must retain the above copyright 19## notice, this list of conditions and the following disclaimer. 20## * Redistributions in binary form must reproduce the above copyright 21## notice, this list of conditions and the following disclaimer in 22## the documentation and/or other materials provided with the 23## distribution. 24## * Neither the name of Nokia Corporation and its Subsidiary(-ies) nor 25## the names of its contributors may be used to endorse or promote 26## products derived from this software without specific prior written 27## permission. 28## 29## THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS 30## "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT 31## LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR 32## A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT 33## OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, 34## SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT 35## LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, 36## DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY 37## THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT 38## (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE 39## OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE." 40## $QT_END_LICENSE$ 41## 42############################################################################# 43 44 45from PyQt5.QtCore import (pyqtProperty, QEasingCurve, QObject, QPoint, QPointF, 46 QPropertyAnimation, QRect, QRectF, QSize, Qt) 47from PyQt5.QtGui import (QBrush, QColor, QIcon, QLinearGradient, QPainter, 48 QPainterPath, QPixmap) 49from PyQt5.QtWidgets import (QApplication, QGraphicsPixmapItem, QGraphicsScene, 50 QListWidgetItem, QWidget) 51 52import easing_rc 53from ui_form import Ui_Form 54 55 56class Animation(QPropertyAnimation): 57 LinearPath, CirclePath = range(2) 58 59 def __init__(self, target, prop): 60 super(Animation, self).__init__(target, prop) 61 62 self.setPathType(Animation.LinearPath) 63 64 def setPathType(self, pathType): 65 self.m_pathType = pathType 66 self.m_path = QPainterPath() 67 68 def updateCurrentTime(self, currentTime): 69 if self.m_pathType == Animation.CirclePath: 70 if self.m_path.isEmpty(): 71 end = self.endValue() 72 start = self.startValue() 73 self.m_path.moveTo(start) 74 self.m_path.addEllipse(QRectF(start, end)) 75 76 dura = self.duration() 77 if dura == 0: 78 progress = 1.0 79 else: 80 progress = (((currentTime - 1) % dura) + 1) / float(dura) 81 82 easedProgress = self.easingCurve().valueForProgress(progress) 83 if easedProgress > 1.0: 84 easedProgress -= 1.0 85 elif easedProgress < 0: 86 easedProgress += 1.0 87 88 pt = self.m_path.pointAtPercent(easedProgress) 89 self.updateCurrentValue(pt) 90 self.valueChanged.emit(pt) 91 else: 92 super(Animation, self).updateCurrentTime(currentTime) 93 94 95# PyQt doesn't support deriving from more than one wrapped class so we use 96# composition and delegate the property. 97class PixmapItem(QObject): 98 def __init__(self, pix): 99 super(PixmapItem, self).__init__() 100 101 self.pixmap_item = QGraphicsPixmapItem(pix) 102 103 def _set_pos(self, pos): 104 self.pixmap_item.setPos(pos) 105 106 pos = pyqtProperty(QPointF, fset=_set_pos) 107 108 109class Window(QWidget): 110 def __init__(self, parent=None): 111 super(QWidget, self).__init__(parent) 112 113 self.m_iconSize = QSize(64, 64) 114 self.m_scene = QGraphicsScene() 115 self.m_ui = Ui_Form() 116 117 self.m_ui.setupUi(self) 118 self.m_ui.easingCurvePicker.setIconSize(self.m_iconSize) 119 self.m_ui.easingCurvePicker.setMinimumHeight(self.m_iconSize.height() + 50) 120 self.m_ui.buttonGroup.setId(self.m_ui.lineRadio, 0) 121 self.m_ui.buttonGroup.setId(self.m_ui.circleRadio, 1) 122 123 dummy = QEasingCurve() 124 self.m_ui.periodSpinBox.setValue(dummy.period()) 125 self.m_ui.amplitudeSpinBox.setValue(dummy.amplitude()) 126 self.m_ui.overshootSpinBox.setValue(dummy.overshoot()) 127 128 self.m_ui.easingCurvePicker.currentRowChanged.connect(self.curveChanged) 129 self.m_ui.buttonGroup.buttonClicked[int].connect(self.pathChanged) 130 self.m_ui.periodSpinBox.valueChanged.connect(self.periodChanged) 131 self.m_ui.amplitudeSpinBox.valueChanged.connect(self.amplitudeChanged) 132 self.m_ui.overshootSpinBox.valueChanged.connect(self.overshootChanged) 133 self.createCurveIcons() 134 135 pix = QPixmap(':/images/qt-logo.png') 136 self.m_item = PixmapItem(pix) 137 self.m_scene.addItem(self.m_item.pixmap_item) 138 self.m_ui.graphicsView.setScene(self.m_scene) 139 140 self.m_anim = Animation(self.m_item, b'pos') 141 self.m_anim.setEasingCurve(QEasingCurve.OutBounce) 142 self.m_ui.easingCurvePicker.setCurrentRow(int(QEasingCurve.OutBounce)) 143 144 self.startAnimation() 145 146 def createCurveIcons(self): 147 pix = QPixmap(self.m_iconSize) 148 painter = QPainter() 149 150 gradient = QLinearGradient(0, 0, 0, self.m_iconSize.height()) 151 gradient.setColorAt(0.0, QColor(240, 240, 240)) 152 gradient.setColorAt(1.0, QColor(224, 224, 224)) 153 154 brush = QBrush(gradient) 155 156 # The original C++ code uses undocumented calls to get the names of the 157 # different curve types. We do the Python equivalant (but without 158 # cheating). 159 curve_types = [(n, c) for n, c in QEasingCurve.__dict__.items() 160 if isinstance(c, QEasingCurve.Type) and c != QEasingCurve.Custom] 161 curve_types.sort(key=lambda ct: ct[1]) 162 163 painter.begin(pix) 164 165 for curve_name, curve_type in curve_types: 166 painter.fillRect(QRect(QPoint(0, 0), self.m_iconSize), brush) 167 168 curve = QEasingCurve(curve_type) 169 170 if curve_type == QEasingCurve.BezierSpline: 171 curve.addCubicBezierSegment(QPointF(0.4, 0.1), 172 QPointF(0.6, 0.9), QPointF(1.0, 1.0)) 173 elif curve_type == QEasingCurve.TCBSpline: 174 curve.addTCBSegment(QPointF(0.0, 0.0), 0, 0, 0) 175 curve.addTCBSegment(QPointF(0.3, 0.4), 0.2, 1, -0.2) 176 curve.addTCBSegment(QPointF(0.7, 0.6), -0.2, 1, 0.2) 177 curve.addTCBSegment(QPointF(1.0, 1.0), 0, 0, 0) 178 179 painter.setPen(QColor(0, 0, 255, 64)) 180 xAxis = self.m_iconSize.height() / 1.5 181 yAxis = self.m_iconSize.width() / 3.0 182 painter.drawLine(0, xAxis, self.m_iconSize.width(), xAxis) 183 painter.drawLine(yAxis, 0, yAxis, self.m_iconSize.height()) 184 185 curveScale = self.m_iconSize.height() / 2.0; 186 187 painter.setPen(Qt.NoPen) 188 189 # Start point. 190 painter.setBrush(Qt.red) 191 start = QPoint(yAxis, 192 xAxis - curveScale * curve.valueForProgress(0)) 193 painter.drawRect(start.x() - 1, start.y() - 1, 3, 3) 194 195 # End point. 196 painter.setBrush(Qt.blue) 197 end = QPoint(yAxis + curveScale, 198 xAxis - curveScale * curve.valueForProgress(1)) 199 painter.drawRect(end.x() - 1, end.y() - 1, 3, 3) 200 201 curvePath = QPainterPath() 202 curvePath.moveTo(QPointF(start)) 203 t = 0.0 204 while t <= 1.0: 205 to = QPointF(yAxis + curveScale * t, 206 xAxis - curveScale * curve.valueForProgress(t)) 207 curvePath.lineTo(to) 208 t += 1.0 / curveScale 209 210 painter.setRenderHint(QPainter.Antialiasing, True) 211 painter.strokePath(curvePath, QColor(32, 32, 32)) 212 painter.setRenderHint(QPainter.Antialiasing, False) 213 214 item = QListWidgetItem() 215 item.setIcon(QIcon(pix)) 216 item.setText(curve_name) 217 self.m_ui.easingCurvePicker.addItem(item) 218 219 painter.end() 220 221 def startAnimation(self): 222 self.m_anim.setStartValue(QPointF(0, 0)) 223 self.m_anim.setEndValue(QPointF(100, 100)) 224 self.m_anim.setDuration(2000) 225 self.m_anim.setLoopCount(-1) 226 self.m_anim.start() 227 228 def curveChanged(self, row): 229 curveType = QEasingCurve.Type(row) 230 self.m_anim.setEasingCurve(curveType) 231 self.m_anim.setCurrentTime(0) 232 233 isElastic = (curveType >= QEasingCurve.InElastic and curveType <= QEasingCurve.OutInElastic) 234 isBounce = (curveType >= QEasingCurve.InBounce and curveType <= QEasingCurve.OutInBounce) 235 236 self.m_ui.periodSpinBox.setEnabled(isElastic) 237 self.m_ui.amplitudeSpinBox.setEnabled(isElastic or isBounce) 238 self.m_ui.overshootSpinBox.setEnabled(curveType >= QEasingCurve.InBack and curveType <= QEasingCurve.OutInBack) 239 240 def pathChanged(self, index): 241 self.m_anim.setPathType(index) 242 243 def periodChanged(self, value): 244 curve = self.m_anim.easingCurve() 245 curve.setPeriod(value) 246 self.m_anim.setEasingCurve(curve) 247 248 def amplitudeChanged(self, value): 249 curve = self.m_anim.easingCurve() 250 curve.setAmplitude(value) 251 self.m_anim.setEasingCurve(curve) 252 253 def overshootChanged(self, value): 254 curve = self.m_anim.easingCurve() 255 curve.setOvershoot(value) 256 self.m_anim.setEasingCurve(curve) 257 258 259if __name__ == '__main__': 260 261 import sys 262 263 app = QApplication(sys.argv) 264 w = Window() 265 w.resize(400, 400) 266 w.show() 267 sys.exit(app.exec_()) 268