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
45import math
46
47from PyQt5.QtCore import (pyqtProperty, pyqtSignal, QDataStream, QDateTime,
48        QEvent, QEventTransition, QFile, QIODevice, QParallelAnimationGroup,
49        QPointF, QPropertyAnimation, qrand, QRectF, QSignalTransition, qsrand,
50        QState, QStateMachine, Qt, QTimer)
51from PyQt5.QtGui import QColor, QPen, QPainter, QPainterPath, QPixmap
52from PyQt5.QtWidgets import (QApplication, QGraphicsItem, QGraphicsObject,
53        QGraphicsScene, QGraphicsTextItem, QGraphicsView)
54
55import stickman_rc
56
57
58class Node(QGraphicsObject):
59    positionChanged = pyqtSignal()
60
61    def __init__(self, pos, parent=None):
62        super(Node, self).__init__(parent)
63
64        self.m_dragging = False
65
66        self.setPos(pos)
67        self.setFlag(QGraphicsItem.ItemSendsGeometryChanges)
68
69    def boundingRect(self):
70        return QRectF(-6.0, -6.0, 12.0, 12.0)
71
72    def paint(self, painter, option, widget):
73        painter.setPen(Qt.white)
74        painter.drawEllipse(QPointF(0.0, 0.0), 5.0, 5.0)
75
76    def itemChange(self, change, value):
77        if change == QGraphicsItem.ItemPositionChange:
78            self.positionChanged.emit()
79
80        return super(Node, self).itemChange(change, value)
81
82    def mousePressEvent(self, event):
83        self.m_dragging = True
84
85    def mouseMoveEvent(self, event):
86        if self.m_dragging:
87            self.setPos(self.mapToParent(event.pos()))
88
89    def mouseReleaseEvent(self, event):
90        self.m_dragging = False
91
92
93Coords = (
94    # Head: 0
95    (0.0, -150.0),
96
97    # Body pentagon, top->bottom, left->right: 1 - 5
98    (0.0, -100.0),
99    (-50.0, -50.0),
100    (50.0, -50.0),
101    (-25.0, 50.0),
102    (25.0, 50.0),
103
104    # Right arm: 6 - 7
105    (-100.0, 0.0),
106    (-125.0, 50.0),
107
108    # Left arm: 8 - 9
109    (100.0, 0.0),
110    (125.0, 50.0),
111
112    # Lower body: 10 - 11
113    (-35.0, 75.0),
114    (35.0, 75.0),
115
116    # Right leg: 12 - 13
117    (-25.0, 200.0),
118    (-30.0, 300.0),
119
120    # Left leg: 14 - 15
121    (25.0, 200.0),
122    (30.0, 300.0))
123
124
125Bones = (
126    # Neck.
127    (0, 1),
128
129    # Body.
130    (1, 2),
131    (1, 3),
132    (1, 4),
133    (1, 5),
134    (2, 3),
135    (2, 4),
136    (2, 5),
137    (3, 4),
138    (3, 5),
139    (4, 5),
140
141    # Right arm.
142    (2, 6),
143    (6, 7),
144
145    # Left arm.
146    (3, 8),
147    (8, 9),
148
149    # Lower body.
150    (4, 10),
151    (4, 11),
152    (5, 10),
153    (5, 11),
154    (10, 11),
155
156    # Right leg.
157    (10, 12),
158    (12, 13),
159
160    # Left leg.
161    (11, 14),
162    (14, 15))
163
164
165class StickMan(QGraphicsObject):
166    def __init__(self):
167        super(StickMan, self).__init__()
168
169        self.m_sticks = True
170        self.m_isDead = False
171        self.m_pixmap = QPixmap('images/head.png')
172        self.m_penColor = QColor(Qt.white)
173        self.m_fillColor = QColor(Qt.black)
174
175        # Set up start position of limbs.
176        self.m_nodes = []
177        for x, y in Coords:
178            node = Node(QPointF(x, y), self)
179            node.positionChanged.connect(self.childPositionChanged)
180            self.m_nodes.append(node)
181
182        self.m_perfectBoneLengths = []
183        for n1, n2 in Bones:
184            node1 = self.m_nodes[n1]
185            node2 = self.m_nodes[n2]
186
187            dist = node1.pos() - node2.pos()
188            self.m_perfectBoneLengths.append(math.hypot(dist.x(), dist.y()))
189
190        self.startTimer(10)
191
192    def childPositionChanged(self):
193        self.prepareGeometryChange()
194
195    def setDrawSticks(self, on):
196        self.m_sticks = on
197
198        for node in self.m_nodes:
199            node.setVisible(on)
200
201    def drawSticks(self):
202        return self.m_sticks
203
204    def boundingRect(self):
205        # Account for head radius of 50.0 plus pen which is 5.0.
206        return self.childrenBoundingRect().adjusted(-55.0, -55.0, 55.0, 55.0)
207
208    def nodeCount(self):
209        return len(self.m_nodes)
210
211    def node(self, idx):
212        if idx >= 0 and idx < len(self.m_nodes):
213            return self.m_nodes[idx]
214
215        return None
216
217    def timerEvent(self, e):
218        self.update()
219
220    def stabilize(self):
221        threshold = 0.001
222
223        for i, (n1, n2) in enumerate(Bones):
224            node1 = self.m_nodes[n1]
225            node2 = self.m_nodes[n2]
226
227            pos1 = node1.pos()
228            pos2 = node2.pos()
229
230            dist = pos1 - pos2
231            length = math.hypot(dist.x(), dist.y())
232            diff = (length - self.m_perfectBoneLengths[i]) / length
233
234            p = dist * (0.5 * diff)
235            if p.x() > threshold and p.y() > threshold:
236                pos1 -= p
237                pos2 += p
238
239                node1.setPos(pos1)
240                node2.setPos(pos2)
241
242    def posFor(self, idx):
243        return self.m_nodes[idx].pos()
244
245    @pyqtProperty(QColor)
246    def penColor(self):
247        return QColor(self.m_penColor)
248
249    @penColor.setter
250    def penColor(self, color):
251        self.m_penColor = QColor(color)
252
253    @pyqtProperty(QColor)
254    def fillColor(self):
255        return QColor(self.m_fillColor)
256
257    @fillColor.setter
258    def fillColor(self, color):
259        self.m_fillColor = QColor(color)
260
261    @pyqtProperty(bool)
262    def isDead(self):
263        return self.m_isDead
264
265    @isDead.setter
266    def isDead(self, isDead):
267        self.m_isDead = isDead
268
269    def paint(self, painter, option, widget):
270        self.stabilize()
271
272        if self.m_sticks:
273            painter.setPen(Qt.white)
274
275            for n1, n2 in Bones:
276                node1 = self.m_nodes[n1]
277                node2 = self.m_nodes[n2]
278
279                painter.drawLine(node1.pos(), node2.pos())
280        else:
281            # First bone is neck and will be used for head.
282
283            path = QPainterPath()
284            path.moveTo(self.posFor(0))
285            path.lineTo(self.posFor(1))
286
287            # Right arm.
288            path.lineTo(self.posFor(2))
289            path.lineTo(self.posFor(6))
290            path.lineTo(self.posFor(7))
291
292            # Left arm.
293            path.moveTo(self.posFor(3))
294            path.lineTo(self.posFor(8))
295            path.lineTo(self.posFor(9))
296
297            # Body.
298            path.moveTo(self.posFor(2))
299            path.lineTo(self.posFor(4))
300            path.lineTo(self.posFor(10))
301            path.lineTo(self.posFor(11))
302            path.lineTo(self.posFor(5))
303            path.lineTo(self.posFor(3))
304            path.lineTo(self.posFor(1))
305
306            # Right leg.
307            path.moveTo(self.posFor(10))
308            path.lineTo(self.posFor(12))
309            path.lineTo(self.posFor(13))
310
311            # Left leg.
312            path.moveTo(self.posFor(11))
313            path.lineTo(self.posFor(14))
314            path.lineTo(self.posFor(15))
315
316            painter.setPen(QPen(self.m_penColor, 5.0, Qt.SolidLine, Qt.RoundCap))
317            painter.drawPath(path)
318
319            n1, n2 = Bones[0]
320            node1 = self.m_nodes[n1]
321            node2 = self.m_nodes[n2]
322
323            dist = node2.pos() - node1.pos()
324
325            sinAngle = dist.x() / math.hypot(dist.x(), dist.y())
326            angle = math.degrees(math.asin(sinAngle))
327
328            headPos = node1.pos()
329            painter.translate(headPos)
330            painter.rotate(-angle)
331
332            painter.setBrush(self.m_fillColor)
333            painter.drawEllipse(QPointF(0, 0), 50.0, 50.0)
334
335            painter.setBrush(self.m_penColor)
336            painter.setPen(QPen(self.m_penColor, 2.5, Qt.SolidLine, Qt.RoundCap))
337
338            # Eyes.
339            if self.m_isDead:
340                painter.drawLine(-30.0, -30.0, -20.0, -20.0)
341                painter.drawLine(-20.0, -30.0, -30.0, -20.0)
342
343                painter.drawLine(20.0, -30.0, 30.0, -20.0)
344                painter.drawLine(30.0, -30.0, 20.0, -20.0)
345            else:
346                painter.drawChord(QRectF(-30.0, -30.0, 25.0, 70.0), 30.0 * 16, 120.0 * 16)
347                painter.drawChord(QRectF(5.0, -30.0, 25.0, 70.0), 30.0 * 16, 120.0 * 16)
348
349            # Mouth.
350            if self.m_isDead:
351                painter.drawLine(-28.0, 2.0, 29.0, 2.0)
352            else:
353                painter.setBrush(QColor(128, 0, 64 ))
354                painter.drawChord(QRectF(-28.0, 2.0 - 55.0 / 2.0, 57.0, 55.0), 0.0, -180.0 * 16)
355
356            # Pupils.
357            if not self.m_isDead:
358                painter.setPen(QPen(self.m_fillColor, 1.0, Qt.SolidLine, Qt.RoundCap))
359                painter.setBrush(self.m_fillColor)
360                painter.drawEllipse(QPointF(-12.0, -25.0), 5.0, 5.0)
361                painter.drawEllipse(QPointF(22.0, -25.0), 5.0, 5.0)
362
363
364class GraphicsView(QGraphicsView):
365    keyPressed = pyqtSignal(int)
366
367    def keyPressEvent(self, e):
368        if e.key() == Qt.Key_Escape:
369            self.close()
370
371        self.keyPressed.emit(Qt.Key(e.key()))
372
373
374class Frame(object):
375    def __init__(self):
376        self.m_nodePositions = []
377
378    def nodeCount(self):
379        return len(self.m_nodePositions)
380
381    def setNodeCount(self, nodeCount):
382        while nodeCount > len(self.m_nodePositions):
383            self.m_nodePositions.append(QPointF())
384
385        while nodeCount < len(self.m_nodePositions):
386            self.m_nodePositions.pop()
387
388    def nodePos(self, idx):
389        return QPointF(self.m_nodePositions[idx])
390
391    def setNodePos(self, idx, pos):
392        self.m_nodePositions[idx] = QPointF(pos)
393
394
395class Animation(object):
396    def __init__(self):
397        self.m_currentFrame = 0
398        self.m_frames = [Frame()]
399        self.m_name = ''
400
401    def setTotalFrames(self, totalFrames):
402        while len(self.m_frames) < totalFrames:
403            self.m_frames.append(Frame())
404
405        while totalFrames < len(self.m_frames):
406            self.m_frames.pop()
407
408    def totalFrames(self):
409        return len(self.m_frames)
410
411    def setCurrentFrame(self, currentFrame):
412        self.m_currentFrame = max(min(currentFrame, self.totalFrames() - 1), 0)
413
414    def currentFrame(self):
415        return self.m_currentFrame
416
417    def setNodeCount(self, nodeCount):
418        frame = self.m_frames[self.m_currentFrame]
419        frame.setNodeCount(nodeCount)
420
421    def nodeCount(self):
422        frame = self.m_frames[self.m_currentFrame]
423        return frame.nodeCount()
424
425    def setNodePos(self, idx, pos):
426        frame = self.m_frames[self.m_currentFrame]
427        frame.setNodePos(idx, pos)
428
429    def nodePos(self, idx):
430        frame = self.m_frames[self.m_currentFrame]
431        return frame.nodePos(idx)
432
433    def name(self):
434        return self.m_name
435
436    def setName(self, name):
437        self.m_name = name
438
439    def save(self, device):
440        stream = QDataStream(device)
441        stream.writeQString(self.m_name)
442        stream.writeInt(len(self.m_frames))
443
444        for frame in self.m_frames:
445            stream.writeInt(frame.nodeCount())
446
447            for i in range(frame.nodeCount()):
448                stream << frame.nodePos(i)
449
450    def load(self, device):
451        self.m_frames = []
452
453        stream = QDataStream(device)
454        self.m_name = stream.readQString()
455        frameCount = stream.readInt()
456
457        for i in range(frameCount):
458            nodeCount = stream.readInt()
459
460            frame = Frame()
461            frame.setNodeCount(nodeCount)
462
463            for j in range(nodeCount):
464                pos = QPointF()
465                stream >> pos
466
467                frame.setNodePos(j, pos)
468
469            self.m_frames.append(frame)
470
471
472class KeyPressTransition(QSignalTransition):
473    def __init__(self, receiver, key, target=None):
474        super(KeyPressTransition, self).__init__(receiver.keyPressed)
475
476        self.m_key = key
477
478        if target is not None:
479            self.setTargetState(target)
480
481    def eventTest(self, e):
482        if super(KeyPressTransition, self).eventTest(e):
483            key = e.arguments()[0]
484            return key == self.m_key
485
486        return False
487
488
489class LightningStrikesTransition(QEventTransition):
490    def __init__(self, target):
491        super(LightningStrikesTransition, self).__init__()
492
493        self.setEventSource(self)
494        self.setEventType(QEvent.Timer)
495        self.setTargetState(target)
496        qsrand(QDateTime.currentDateTime().toTime_t())
497        self.startTimer(1000)
498
499    def eventTest(self, e):
500        return (super(LightningStrikesTransition, self).eventTest(e) and
501                (qrand() % 50) == 0)
502
503
504class LifeCycle(object):
505    def __init__(self, stickMan, keyReceiver):
506        self.m_stickMan = stickMan
507        self.m_keyReceiver = keyReceiver
508
509        # Create animation group to be used for all transitions.
510        self.m_animationGroup = QParallelAnimationGroup()
511        stickManNodeCount = self.m_stickMan.nodeCount()
512        self._pas = []
513        for i in range(stickManNodeCount):
514            pa = QPropertyAnimation(self.m_stickMan.node(i), b'pos')
515            self._pas.append(pa)
516            self.m_animationGroup.addAnimation(pa)
517
518        # Set up intial state graph.
519        self.m_machine = QStateMachine()
520        self.m_machine.addDefaultAnimation(self.m_animationGroup)
521
522        self.m_alive = QState(self.m_machine)
523        self.m_alive.setObjectName('alive')
524
525        # Make it blink when lightning strikes before entering dead animation.
526        lightningBlink = QState(self.m_machine)
527        lightningBlink.assignProperty(self.m_stickMan.scene(),
528                'backgroundBrush', Qt.white)
529        lightningBlink.assignProperty(self.m_stickMan, 'penColor', Qt.black)
530        lightningBlink.assignProperty(self.m_stickMan, 'fillColor', Qt.white)
531        lightningBlink.assignProperty(self.m_stickMan, 'isDead', True)
532
533        timer = QTimer(lightningBlink)
534        timer.setSingleShot(True)
535        timer.setInterval(100)
536        lightningBlink.entered.connect(timer.start)
537        lightningBlink.exited.connect(timer.stop)
538
539        self.m_dead = QState(self.m_machine)
540        self.m_dead.assignProperty(self.m_stickMan.scene(), 'backgroundBrush',
541                Qt.black)
542        self.m_dead.assignProperty(self.m_stickMan, 'penColor', Qt.white)
543        self.m_dead.assignProperty(self.m_stickMan, 'fillColor', Qt.black)
544        self.m_dead.setObjectName('dead')
545
546        # Idle state (sets no properties).
547        self.m_idle = QState(self.m_alive)
548        self.m_idle.setObjectName('idle')
549
550        self.m_alive.setInitialState(self.m_idle)
551
552        # Lightning strikes at random.
553        self.m_alive.addTransition(LightningStrikesTransition(lightningBlink))
554        lightningBlink.addTransition(timer.timeout, self.m_dead)
555
556        self.m_machine.setInitialState(self.m_alive)
557
558    def setDeathAnimation(self, fileName):
559        deathAnimation = self.makeState(self.m_dead, fileName)
560        self.m_dead.setInitialState(deathAnimation)
561
562    def start(self):
563        self.m_machine.start()
564
565    def addActivity(self, fileName, key):
566        state = self.makeState(self.m_alive, fileName)
567        self.m_alive.addTransition(KeyPressTransition(self.m_keyReceiver, key, state))
568
569    def makeState(self, parentState, animationFileName):
570        topLevel = QState(parentState)
571
572        animation = Animation()
573
574        file = QFile(animationFileName)
575        if file.open(QIODevice.ReadOnly):
576            animation.load(file)
577
578        frameCount = animation.totalFrames()
579        previousState = None
580        for i in range(frameCount):
581            animation.setCurrentFrame(i)
582
583            frameState = QState(topLevel)
584            nodeCount = animation.nodeCount()
585            for j in range(nodeCount):
586                frameState.assignProperty(self.m_stickMan.node(j), 'pos',
587                        animation.nodePos(j))
588
589            frameState.setObjectName('frame %d' % i)
590
591            if previousState is None:
592                topLevel.setInitialState(frameState)
593            else:
594                previousState.addTransition(previousState.propertiesAssigned,
595                        frameState)
596
597            previousState = frameState
598
599        previousState.addTransition(previousState.propertiesAssigned,
600                topLevel.initialState())
601
602        return topLevel
603
604
605if __name__ == '__main__':
606
607    import sys
608
609    app = QApplication(sys.argv)
610
611    stickMan = StickMan()
612    stickMan.setDrawSticks(False)
613
614    textItem = QGraphicsTextItem()
615    textItem.setHtml("<font color=\"white\"><b>Stickman</b>"
616        "<p>"
617        "Tell the stickman what to do!"
618        "</p>"
619        "<p><i>"
620        "<li>Press <font color=\"purple\">J</font> to make the stickman jump.</li>"
621        "<li>Press <font color=\"purple\">D</font> to make the stickman dance.</li>"
622        "<li>Press <font color=\"purple\">C</font> to make him chill out.</li>"
623        "<li>When you are done, press <font color=\"purple\">Escape</font>.</li>"
624        "</i></p>"
625        "<p>If he is unlucky, the stickman will get struck by lightning, and never jump, dance or chill out again."
626        "</p></font>")
627
628    w = textItem.boundingRect().width()
629    stickManBoundingRect = stickMan.mapToScene(stickMan.boundingRect()).boundingRect()
630    textItem.setPos(-w / 2.0, stickManBoundingRect.bottom() + 25.0)
631
632    scene = QGraphicsScene()
633    scene.addItem(stickMan)
634    scene.addItem(textItem)
635    scene.setBackgroundBrush(Qt.black)
636
637    view = GraphicsView()
638    view.setRenderHints(QPainter.Antialiasing)
639    view.setTransformationAnchor(QGraphicsView.NoAnchor)
640    view.setScene(scene)
641    view.show()
642    view.setFocus()
643
644    # Make enough room in the scene for stickman to jump and die.
645    sceneRect = scene.sceneRect()
646    view.resize(sceneRect.width() + 100, sceneRect.height() + 100)
647    view.setSceneRect(sceneRect)
648
649    cycle = LifeCycle(stickMan, view)
650    cycle.setDeathAnimation(':/animations/dead')
651
652    cycle.addActivity(':/animations/jumping', Qt.Key_J)
653    cycle.addActivity(':/animations/dancing', Qt.Key_D)
654    cycle.addActivity(':/animations/chilling', Qt.Key_C)
655    cycle.start()
656
657    sys.exit(app.exec_())
658