diff --git a/.gitignore b/.gitignore index 7cec615..916c6c1 100644 --- a/.gitignore +++ b/.gitignore @@ -13,3 +13,4 @@ env/* ffmpeg *.bak *~ +*.goutput* \ No newline at end of file diff --git a/src/components/__template__.ui b/src/components/__template__.ui new file mode 100644 index 0000000..301a2b7 --- /dev/null +++ b/src/components/__template__.ui @@ -0,0 +1,119 @@ + + + Form + + + + 0 + 0 + 586 + 197 + + + + Form + + + + + + + + Qt::Horizontal + + + + 40 + 20 + + + + + + + + + + + + Qt::Horizontal + + + + 40 + 20 + + + + + + + + + + + + Qt::Horizontal + + + + 40 + 20 + + + + + + + + + + + + Qt::Horizontal + + + + 40 + 20 + + + + + + + + + + + + Qt::Horizontal + + + + 40 + 20 + + + + + + + + + + Qt::Vertical + + + + 20 + 40 + + + + + + + + + diff --git a/src/components/life.py b/src/components/life.py new file mode 100644 index 0000000..08360a2 --- /dev/null +++ b/src/components/life.py @@ -0,0 +1,348 @@ +from PyQt5 import QtGui, QtCore, QtWidgets +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.0a' + + def widget(self, *args): + super().widget(*args) + self.scale = 32 + self.updateGridSize() + self.startingGrid = {} + 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, + 'image': self.page.lineEdit_image, + }, colorWidgets={ + 'color': self.page.pushButton_color, + }) + 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.page.lineEdit_image.setText(filename) + self.update() + + 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) + super().update() + + 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 + ) + if button == 1: + self.startingGrid[pos] = True + elif button == 2 and pos in self.startingGrid: + self.startingGrid.pop(pos) + + 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, len(self.completeAudioArray), 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/len(self.completeAudioArray))) + 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 nearbyCoords(x, y): + if grid.get(cell) is None: + 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) + slantLine = lambda difference: ( + ((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 + return frame + + def gridForTick(self, tick): + '''Given a tick number over 0, returns a new grid dict of tuples''' + lastGrid = self.tickGrids[tick - 1] + + def neighbours(x, y): + return [ + cell for cell in nearbyCoords(x, y) + if lastGrid.get(cell) is not None + ] + + newGrid = {} + for x, y in lastGrid: + surrounding = len(neighbours(x, y)) + if surrounding == 2 or surrounding == 3: + newGrid[(x, y)] = True + potentialNewCells = set([ + coordTup for origin in lastGrid + for coordTup in list(nearbyCoords(*origin)) + ]) + for x, y in potentialNewCells: + if (x, y) in newGrid: + continue + surrounding = len(neighbours(x, y)) + if surrounding == 3: + newGrid[(x, y)] = True + + return newGrid + + def savePreset(self): + pr = super().savePreset() + pr['GRID'] = self.startingGrid + return pr + + def loadPreset(self, pr, *args): + super().loadPreset(pr, *args) + self.startingGrid = pr['GRID'] + + +def nearbyCoords(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 diff --git a/src/components/life.ui b/src/components/life.ui new file mode 100644 index 0000000..3b393dd --- /dev/null +++ b/src/components/life.ui @@ -0,0 +1,358 @@ + + + Form + + + + 0 + 0 + 586 + 197 + + + + Form + + + + + + + + + + + + Simulation Speed + + + + + + + frames per tick + + + 1 + + + 30 + + + 5 + + + + + + + Qt::Horizontal + + + + 40 + 20 + + + + + + + + + 0 + 16777215 + + + + 0,0,0 + + + + + + + + + + + Grid Scale + + + + + + + 24 + + + 128 + + + 32 + + + + + + + Custom Image + + + + + + + Qt::Horizontal + + + + 40 + 20 + + + + + + + + + + + + Image + + + + + + + + + + + 0 + 0 + + + + + 32 + 32 + + + + ... + + + + + + + Color + + + + + + + + 0 + 16777215 + + + + 0,0,0 + + + + + + + + 0 + 0 + + + + + 32 + 32 + + + + + + + false + + + false + + + + + + + Qt::Horizontal + + + + 40 + 20 + + + + + + + + Shape + + + + + + + + Path + + + + + Rectangle + + + + + Elliptical + + + + + Circle + + + + + Lilypad + + + + + Pac-Man + + + + + Duck + + + + + Peace + + + + + + + + Qt::Horizontal + + + + 40 + 20 + + + + + + + + + + + + Shadow + + + + + + + Qt::Horizontal + + + + 40 + 20 + + + + + + + + + + + + Qt::Horizontal + + + + 40 + 20 + + + + + + + + + + + + <!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.0//EN" "http://www.w3.org/TR/REC-html40/strict.dtd"> +<html><head><meta name="qrichtext" content="1" /><style type="text/css"> +p, li { white-space: pre-wrap; } +</style></head><body style=" font-family:'Ubuntu'; font-size:11pt; font-weight:400; font-style:normal;"> +<p style=" margin-top:12px; margin-bottom:12px; margin-left:0px; margin-right:0px; -qt-block-indent:0; text-indent:0px;"><span style=" font-weight:600;">Click the preview window to place a cell. Right-click to remove.</span></p> +<p style=" margin-top:12px; margin-bottom:12px; margin-left:0px; margin-right:0px; -qt-block-indent:0; text-indent:0px;">- A cell with less than 2 neighbours will die from underpopulation</p> +<p style=" margin-top:12px; margin-bottom:12px; margin-left:0px; margin-right:0px; -qt-block-indent:0; text-indent:0px;">- A cell with more than 3 neighbours will die from overpopulation.</p> +<p style=" margin-top:12px; margin-bottom:12px; margin-left:0px; margin-right:0px; -qt-block-indent:0; text-indent:0px;">- An empty space surrounded by 3 live cells will cause reproduction.</p></body></html> + + + 80 + + + Qt::NoTextInteraction + + + false + + + + + + + + + Qt::Vertical + + + + 20 + 40 + + + + + + + + + diff --git a/src/mainwindow.py b/src/mainwindow.py index 1c8806d..789a6e7 100644 --- a/src/mainwindow.py +++ b/src/mainwindow.py @@ -50,6 +50,22 @@ class PreviewWindow(QtWidgets.QLabel): self.pixmap = QtGui.QPixmap(img) self.repaint() + def mousePressEvent(self, event): + if self.parent.encoding: + return + + i = self.parent.window.listWidget_componentList.currentRow() + if i >= 0: + component = self.parent.core.selectedComponents[i] + if not hasattr(component, 'previewClickEvent'): + return + pos = (event.x(), event.y()) + size = (self.width(), self.height()) + component.previewClickEvent( + pos, size, event.button() + ) + self.parent.core.updateComponent(i) + @QtCore.pyqtSlot(str) def threadError(self, msg): self.parent.showMessage(