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