This repository has been archived on 2020-08-22. You can view files and clone it, but cannot push or open issues or pull requests.
pyaudviz/src/components/life.py

476 lines
16 KiB
Python
Raw Normal View History

2017-08-09 16:46:59 -04:00
from PyQt5 import QtGui, QtCore, QtWidgets
2017-08-27 19:59:51 -04:00
from PyQt5.QtWidgets import QUndoCommand
from PIL import Image, ImageDraw, ImageEnhance, ImageChops, ImageFilter
2017-08-09 16:46:59 -04:00
import os
import math
from component import Component
from toolkit.frame import BlankFrame, scale
2017-08-09 16:46:59 -04:00
class Component(Component):
name = 'Conway\'s Game of Life'
version = '1.0.0'
2017-08-09 16:46:59 -04:00
def widget(self, *args):
super().widget(*args)
self.scale = 32
self.updateGridSize()
2017-08-11 19:03:10 -04:00
self.startingGrid = set()
self.page.pushButton_pickImage.clicked.connect(self.pickImage)
2017-08-09 16:46:59 -04:00
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,
2017-08-09 16:46:59 -04:00
}, 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])
2017-08-09 16:46:59 -04:00
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
2017-08-09 16:46:59 -04:00
def shiftGrid(self, d):
2017-08-27 19:59:51 -04:00
action = ShiftGrid(self, d)
self.parent.undoStack.push(action)
2017-08-09 16:46:59 -04:00
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)
2017-08-09 16:46:59 -04:00
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
)
2017-08-27 19:59:51 -04:00
action = ClickGrid(self, pos, button)
self.parent.undoStack.push(action)
2017-08-09 16:46:59 -04:00
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
2017-08-09 16:46:59 -04:00
):
if self.parent.canceled:
break
2017-08-09 16:46:59 -04:00
if frameNo % self.tickRate == 0:
tick += 1
self.tickGrids[tick] = self.gridForTick(tick)
# update progress bar
progress = int(100*(frameNo/self.audioArrayLen))
2017-08-09 16:46:59 -04:00
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."
2017-08-09 16:46:59 -04:00
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)
2017-08-10 09:12:48 -04:00
rect = (
(drawPtX, drawPtY),
(drawPtX + self.pxWidth, drawPtY + self.pxHeight)
)
shape = self.page.comboBox_shapeType.currentText().lower()
# Rectangle
2017-08-10 09:12:48 -04:00
if shape == 'rectangle':
drawer.rectangle(rect, fill=self.color)
2017-08-10 09:12:48 -04:00
# 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
2017-08-10 09:12:48 -04:00
if shape == 'circle':
drawer.ellipse(outlineShape, fill=self.color)
drawer.ellipse(smallerShape, fill=(0, 0, 0, 0))
# Lilypad
2017-08-10 09:12:48 -04:00
elif shape == 'lilypad':
drawer.pieslice(smallerShape, 290, 250, fill=self.color)
# Pac-Man
2017-08-10 09:12:48 -04:00
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
2017-08-10 09:12:48 -04:00
# Path
if shape == 'path':
drawer.ellipse(rect, fill=self.color)
rects = {
direction: False
for direction in (
'up', 'down', 'left', 'right',
)
}
2017-08-27 19:59:51 -04:00
for cell in self.nearbyCoords(x, y):
2017-08-11 19:03:10 -04:00
if cell not in grid:
2017-08-10 09:12:48 -04:00
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)
2017-08-10 09:12:48 -04:00
)
elif direction == 'left':
sect = (
(drawPtX, drawPtY),
(drawPtX + hX,
drawPtY + self.pxHeight)
2017-08-10 09:12:48 -04:00
)
elif direction == 'right':
sect = (
(drawPtX + hX, drawPtY),
(drawPtX + self.pxWidth,
drawPtY + self.pxHeight)
2017-08-10 09:12:48 -04:00
)
drawer.rectangle(sect, fill=self.color)
# Duck
2017-08-10 09:12:48 -04:00
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
2017-08-10 09:12:48 -04:00
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
)
2017-08-09 16:46:59 -04:00
for x, y in grid:
drawPtX = x * self.pxWidth
if drawPtX > self.width:
continue
2017-08-09 16:46:59 -04:00
drawPtY = y * self.pxHeight
if drawPtY > self.height:
continue
if self.customImg:
drawCustomImg()
else:
drawShape()
2017-08-09 16:46:59 -04:00
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,
)
2017-08-09 16:46:59 -04:00
return frame
def gridForTick(self, tick):
2017-08-11 19:03:10 -04:00
'''Given a tick number over 0, returns a new grid set of tuples'''
2017-08-09 16:46:59 -04:00
lastGrid = self.tickGrids[tick - 1]
def neighbours(x, y):
2017-08-11 19:03:10 -04:00
return {
2017-08-27 19:59:51 -04:00
cell for cell in self.nearbyCoords(x, y)
2017-08-11 19:03:10 -04:00
if cell in lastGrid
}
2017-08-09 16:46:59 -04:00
2017-08-11 19:03:10 -04:00
newGrid = set()
2017-08-09 16:46:59 -04:00
for x, y in lastGrid:
surrounding = len(neighbours(x, y))
if surrounding == 2 or surrounding == 3:
2017-08-11 19:03:10 -04:00
newGrid.add((x, y))
potentialNewCells = {
2017-08-09 16:46:59 -04:00
coordTup for origin in lastGrid
2017-08-27 19:59:51 -04:00
for coordTup in list(self.nearbyCoords(*origin))
2017-08-11 19:03:10 -04:00
}
2017-08-09 16:46:59 -04:00
for x, y in potentialNewCells:
if (x, y) in newGrid:
continue
surrounding = len(neighbours(x, y))
if surrounding == 3:
2017-08-11 19:03:10 -04:00
newGrid.add((x, y))
2017-08-09 16:46:59 -04:00
return newGrid
def savePreset(self):
pr = super().savePreset()
2017-08-11 19:03:10 -04:00
pr['GRID'] = sorted(self.startingGrid)
2017-08-09 16:46:59 -04:00
return pr
def loadPreset(self, pr, *args):
2017-08-11 19:03:10 -04:00
self.startingGrid = set(pr['GRID'])
if self.startingGrid:
for widget in self.shiftButtons:
widget.setEnabled(True)
super().loadPreset(pr, *args)
2017-08-10 09:12:48 -04:00
2017-08-27 19:59:51 -04:00
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
}
2017-08-10 09:12:48 -04:00
2017-08-27 19:59:51 -04:00
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()