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