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(