from PyQt5 import QtGui, QtCore, QtWidgets from PyQt5.QtWidgets import QUndoCommand from PIL import Image, ImageDraw, ImageEnhance, ImageChops, ImageFilter import os import math from component import Component from toolkit.frame import BlankFrame, scale 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() self.startingGrid = set() 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.progressBarSetText.emit("Computing evolution...") self.tickGrids = {0: self.startingGrid} tick = 0 for frameNo in range( self.tickRate, self.audioArrayLen, self.sampleSize ): if self.parent.canceled: break if frameNo % self.tickRate == 0: tick += 1 self.tickGrids[tick] = self.gridForTick(tick) # update progress bar progress = int(100*(frameNo/self.audioArrayLen)) if progress >= 100: progress = 100 pStr = "Computing evolution: "+str(progress)+'%' self.progressBarSetText.emit(pStr) self.progressBarUpdate.emit(int(progress)) 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) grid = self.tickGrids[tick] 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.ANTIALIAS) 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) # Pac-Man elif shape == 'pac-man': 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 set of tuples''' lastGrid = self.tickGrids[tick - 1] def neighbours(x, y): return { cell for cell in self.nearbyCoords(x, y) if cell in lastGrid } newGrid = set() for x, y in lastGrid: surrounding = len(neighbours(x, y)) if surrounding == 2 or surrounding == 3: newGrid.add((x, y)) potentialNewCells = { coordTup for origin in lastGrid for coordTup in list(self.nearbyCoords(*origin)) } for x, y in potentialNewCells: if (x, y) in newGrid: 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, id_): super().__init__( "click %s component #%s" % (comp.name, comp.compPos)) self.comp = comp self.pos = [pos] self.id_ = id_ def id(self): return self.id_ 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.id_ == 1: # Left-click self.add() elif self.id_ == 2: # Right-click self.remove() def undo(self): if self.id_ == 1: # Left-click self.remove() elif self.id_ == 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()