commit 74bdd88766ad9f3a451de73ca4d07ac1a7b15b96 Author: Lucio Andrés Illanes Albornoz Date: Wed Jun 27 17:09:32 2018 +0200 Initial commit. diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..7a00c01 --- /dev/null +++ b/.gitignore @@ -0,0 +1,2 @@ +__pycache__/ +*.sw[op] diff --git a/DejaVuSansMono.ttf b/DejaVuSansMono.ttf new file mode 100644 index 0000000..f578602 Binary files /dev/null and b/DejaVuSansMono.ttf differ diff --git a/ENNTool.py b/ENNTool.py new file mode 100755 index 0000000..394ae31 --- /dev/null +++ b/ENNTool.py @@ -0,0 +1,143 @@ +#!/usr/bin/env python3 +# +# ENNTool -- mIRC art animation tool (for EFnet #MiRCART) (WIP) +# Copyright (c) 2018 Lucio Andrés Illanes Albornoz +# This project is licensed under the terms of the MIT license. +# +# TODO: +# 1) -A: render frame #1, render frame #2, ... +# 2) -o: support at least {GIF,MP4,WEBM} +# 3) -s: effects: rotate, smash into bricks, swirl, wave, ... +# 4) XXX autodetect video width from widest mircart +# 5) XXX convert TTF to texture PNG & coords TXT, render accordingly +# 6) XXX dont stall GPU w/ glReadPixels(), switch to asynchronous model w/ PBOs +# 7) XXX render mircart as 3D blocks vs flat surface +# + +from getopt import getopt, GetoptError +from glob import glob +from OpenGL.GL import * +import os, sys, time +import wx + +from ENNToolGLCanvasPanel import ENNToolGLCanvasPanel +from ENNToolMiRCARTImporter import ENNToolMiRCARTImporter + +class ENNToolApp(object): + """XXX""" + + # {{{ parseArgv(self, argv): XXX + def parseArgv(self, argv): + def usage(argv0): + print("usage: {}".format(os.path.basename(argv0)), file=sys.stderr) + print(" [-A] [-f fps] [-h] [-o pname]".format(os.path.basename(argv0)), file=sys.stderr) + print(" [-p] [-r WxH] [-R WxH] [-s pname]", file=sys.stderr) + print(" [-S] [-t pname] [-v] [--] pname..", file=sys.stderr) + print("", file=sys.stderr) + print(" -a........: select animation mode", file=sys.stderr) + print(" -f fps....: set video FPS; defaults to 25", file=sys.stderr) + print(" -h........: show this screen", file=sys.stderr) + print(" -o pname..: output video pathname", file=sys.stderr) + print(" -p........: play video after rendering", file=sys.stderr) + print(" -r WxH....: set video resolution; defaults to 1152x864", file=sys.stderr) + print(" -R WxH....: set MiRCART cube resolution; defaults to 0.1x0.2", file=sys.stderr) + print(" -s pname..: input script pathname", file=sys.stderr) + print(" -S........: select scrolling mode", file=sys.stderr) + print(" -t pname..: set MiRCART texture pathname; defaults to texture.png", file=sys.stderr) + print(" -v........: be verbose", file=sys.stderr) + try: + optlist, argv = getopt(argv[1:], "Af:ho:pr:R:s:St:v") + optdict = dict(optlist) + + if "-h" in optdict: + usage(sys.argv[0]); exit(0); + elif not "-o" in optdict: + raise GetoptError("-o pname must be specified") + elif not len(argv): + raise GetoptError("at least one MiRCART input pname must be specified") + + if not "-f" in optdict: + optdict["-f"] = "25" + if not "-r" in optdict: + optdict["-r"] = "1152x864" + if not "-R" in optdict: + optdict["-R"] = "0.1x0.2" + if not "-t" in optdict: + optdict["-t"] = "texture.png" + + if "-r" in optdict: + optdict["-r"] = [int(r) for r in optdict["-r"].split("x")][0:2] + if "-R" in optdict: + optdict["-R"] = [float(r) for r in optdict["-R"].split("x")][0:2] + except GetoptError as e: + print(e.msg); usage(sys.argv[0]); exit(1); + return argv, optdict + # }}} + # {{{ printProgress(self, progressCur, progressMax): XXX + def printProgress(self, progressCur, progressMax): + progressDiv = float(progressCur / progressMax) + if progressDiv >= 1: + progressDiv = 1; endChar = "\n"; + else: + endChar = "" + print("\r[{:<50}] {}%".format( + ("=" * int(progressDiv * 50)), int(progressDiv * 100)), end=endChar) + # }}} + # {{{ modeScroll(self, argv, optdict, panelGLCanvas, texturePathName, fps=25, scrollRate=0.25): XXX + def modeScroll(self, argv, optdict, panelGLCanvas, texturePathName, fps=25, scrollRate=0.25): + MiRCART = [] + for inFileArg in argv: + for inFile in sorted(glob(inFileArg)): + MiRCART += ENNToolMiRCARTImporter(inFile).outMap + + curY, rotateX, rotateY, translateY = 0, 0, 0, scrollRate + artTextureId = panelGLCanvas.initTexture(texturePathName) + artVbo, artVboLen, lastY, numVertices = panelGLCanvas.renderMiRCART(MiRCART, cubeSize=optdict["-R"]) + if "-v" in optdict: + print("{} vertices".format(numVertices)) + w, h = panelGLCanvas.GetClientSize(); w, h = max(w, 1.0), max(h, 1.0); + + while True: + self.printProgress(curY, lastY) + for numFrame in range(fps): + panelGLCanvas.renderFrame(artTextureId, artVbo, artVboLen) + if translateY: + glTranslatef(0, translateY, 0); curY += translateY + if rotateX: + glRotatef(rotateX * (180.0/w), 0.0, 1.0, 0.0) + if rotateY: + glRotatef(rotateY * (180.0/h), 1.0, 0.0, 0.0) + panelGLCanvas.saveFrame() + if curY >= lastY: + self.printProgress(curY, lastY); break; + # }}} + # {{{ __init__(self, argv): XXX + def __init__(self, argv): + argv, optdict = self.parseArgv(argv) + wxApp = wx.App(False) + appFrameSize = optdict["-r"] + appFrame = wx.Frame(None, size=appFrameSize); appFrame.Hide(); + appPanelSkin = wx.Panel(appFrame, wx.ID_ANY) + + videoFps, videoPath = int(optdict["-f"]), optdict["-o"] + panelGLCanvas = ENNToolGLCanvasPanel(appPanelSkin, size=appFrameSize, videoPath=videoPath) + panelGLCanvas.initOpenGL(); panelGLCanvas.initVideoWriter(fps=videoFps) + + if "-v" in optdict: + time0 = time.time() + self.modeScroll(argv, optdict, panelGLCanvas, fps=videoFps, texturePathName=optdict["-t"]) + if "-v" in optdict: + print("delta {}s".format(time.time() - time0)) + if "-p" in optdict: + os.startfile(videoPath) + # }}} + +# +# Entry point +def main(*argv): + ENNToolApp(argv) + +if __name__ == "__main__": + main(*sys.argv) + +# vim:expandtab foldmethod=marker sw=4 ts=4 tw=120 diff --git a/ENNToolGLCanvasPanel.py b/ENNToolGLCanvasPanel.py new file mode 100644 index 0000000..5b6abfe --- /dev/null +++ b/ENNToolGLCanvasPanel.py @@ -0,0 +1,156 @@ +#!/usr/bin/env python3 +# +# ENNTool -- mIRC art animation tool (for EFnet #MiRCART) (WIP) +# Copyright (c) 2018 Lucio Andrés Illanes Albornoz +# This project is licensed under the terms of the MIT license. +# +# References: +# Wed, 27 Jun 2018 16:02:10 +0200 [1] +# Wed, 27 Jun 2018 16:02:11 +0200 [2] +# Wed, 27 Jun 2018 16:02:12 +0200 [3] +# Wed, 27 Jun 2018 16:02:13 +0200 [4] +# Wed, 27 Jun 2018 16:02:14 +0200 [5] +# + +from OpenGL.GL import * +from PIL import Image +import cv2, numpy +import ctypes, sys +import wx, wx.glcanvas + +class ENNToolGLCanvasPanel(wx.glcanvas.GLCanvas, wx.Panel): + """XXX""" + + # {{{ initOpenGL(self): XXX + def initOpenGL(self): + self.glContext = wx.glcanvas.GLContext(self) + self.SetCurrent(self.glContext) + + # [1] + glViewport(0, 0, *self.curSize) + glMatrixMode(GL_PROJECTION) + glLoadIdentity(); glFrustum(-1, 1, -1, 1, 1, 100); + glMatrixMode(GL_MODELVIEW) + glEnable(GL_DEPTH_TEST) + glClearColor(0, 0, 0, 1); glClearDepth(1); + glTranslatef(-5.0, 3.0, -5) + + # [3] + glLight(GL_LIGHT0, GL_AMBIENT, (0, 0, 0, 1)) + glLight(GL_LIGHT0, GL_DIFFUSE, (1, 1, 1, 1)) + glLight(GL_LIGHT0, GL_POSITION, (1, 1, 0+2, 0)) + glLight(GL_LIGHT0, GL_SPECULAR, (1, 1, 1, 1)) + glLightModelfv(GL_LIGHT_MODEL_AMBIENT, (0.2, 0.2, 0.2, 1)) + glEnable(GL_LIGHTING); glEnable(GL_LIGHT0); + glEnable(GL_COLOR_MATERIAL) + glColorMaterial(GL_FRONT_AND_BACK, GL_AMBIENT_AND_DIFFUSE) + # }}} + # {{{ initTexture(self, pathName): XXX + def initTexture(self, pathName): + artTextureId = glGenTextures(1) + artTextureImage = Image.open(pathName) + artTextureImageData = numpy.array(list(artTextureImage.getdata()), numpy.uint8) + + glBindTexture(GL_TEXTURE_2D, artTextureId) + glPixelStorei(GL_UNPACK_ALIGNMENT, 1) + glTexParameterf(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_CLAMP) + glTexParameterf(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_CLAMP) + + # [2] + glTexParameterf(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_NEAREST) + glTexParameterf(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_NEAREST) + + # [4][5] + glTexImage2D(GL_TEXTURE_2D, 0, GL_RGB, + artTextureImage.size[0], artTextureImage.size[1], + 0, GL_RGBA, GL_UNSIGNED_BYTE, artTextureImageData) + glBindTexture(GL_TEXTURE_2D, artTextureId) + return artTextureId + # }}} + # {{{ initVideoWriter(self): XXX + def initVideoWriter(self, fourcc="XVID", fps=25): + fourcc = cv2.VideoWriter_fourcc(*list(fourcc)) + self.videoWriter = cv2.VideoWriter(self.videoPath, fourcc, fps, (self.width, self.height), True) + # }}} + # {{{ renderFrame(self, artTextureId, artVbo, artVboLen): XXX + def renderFrame(self, artTextureId, artVbo, artVboLen): + glEnableClientState(GL_VERTEX_ARRAY) + glEnableClientState(GL_NORMAL_ARRAY) + glEnableClientState(GL_TEXTURE_COORD_ARRAY) + glEnable(GL_TEXTURE_2D) + + glClear(GL_COLOR_BUFFER_BIT|GL_DEPTH_BUFFER_BIT) + + glBindTexture(GL_TEXTURE_2D, artTextureId) + glBindBuffer(GL_ARRAY_BUFFER, artVbo) + glVertexPointer(3, GL_FLOAT, 32, ctypes.c_void_p(0)) + glNormalPointer(GL_FLOAT, 32, ctypes.c_void_p(12)) + glTexCoordPointer(2, GL_FLOAT, 32, ctypes.c_void_p(24)) + glDrawArrays(GL_QUADS, 0, artVboLen) + + glDisable(GL_TEXTURE_2D) + glDisableClientState(GL_TEXTURE_COORD_ARRAY) + glDisableClientState(GL_NORMAL_ARRAY) + glDisableClientState(GL_VERTEX_ARRAY) + # }}} + # {{{ renderMiRCART(self, artMap, centre=True, canvasCols=100, cubeSize=(0.1, 0.2), texelHeight=0.0625, texelWidth=0.0625): XXX + def renderMiRCART(self, artMap, centre=True, canvasCols=100, cubeSize=(0.1, 0.2), texelHeight=0.0625, texelWidth=0.0625): + curPos = [0, 0, 0]; vertices = []; numVertices = 0; + for numRow in range(len(artMap)): + if centre and (len(artMap[numRow]) < canvasCols): + curPos[0] += (((canvasCols - len(artMap[numRow])) * cubeSize[0]) / 2) + for numCol in range(len(artMap[numRow])): + cubeColour = artMap[numRow][numCol][1] * texelWidth + + # Top Right + vertices += curPos + vertices += [0.0, 0.0, 1.0] + vertices += [cubeColour+texelWidth, texelHeight] + numVertices += 1 + + # Top Left + vertices += [curPos[0]-cubeSize[0], curPos[1], curPos[2]] + vertices += [0.0, 0.0, 1.0] + vertices += [cubeColour, texelHeight] + numVertices += 1 + + # Bottom Left + vertices += [curPos[0]-cubeSize[0], curPos[1]-cubeSize[1], curPos[2]] + vertices += [0.0, 0.0, 1.0] + vertices += [cubeColour, 0.0] + numVertices += 1 + + # Bottom Right + vertices += [curPos[0], curPos[1]-cubeSize[1], curPos[2]] + vertices += [0.0, 0.0, 1.0] + vertices += [cubeColour+texelWidth, 0.0] + numVertices += 1 + + curPos[0] += cubeSize[0] + curPos[0], curPos[1] = 0, curPos[1] - cubeSize[1] + + artVbo = glGenBuffers(1) + glBindBuffer(GL_ARRAY_BUFFER, artVbo) + glBufferData(GL_ARRAY_BUFFER, + (ctypes.c_float*len(vertices))(*vertices), + GL_STATIC_DRAW) + return artVbo, len(vertices), -curPos[1], numVertices + # }}} + # {{{ saveFrame(self): XXX + def saveFrame(self): + if sys.byteorder == "little": + screenshot = glReadPixels(0, 0, self.width, self.height, GL_BGR, GL_UNSIGNED_BYTE) + else: + screenshot = glReadPixels(0, 0, self.width, self.height, GL_RGB, GL_UNSIGNED_BYTE) + screenshot = numpy.flipud(numpy.frombuffer(screenshot, numpy.uint8).reshape((self.height, self.width, 3))) + self.videoWriter.write(screenshot) + # }}} + # {{{ __init__(self, parent, size, defaultPos=(24,24), videoPath=None): initialisation method + def __init__(self, parent, size, defaultPos=(24,24), videoPath=None): + super().__init__(parent, pos=defaultPos, size=size) + self.curPos = list(defaultPos); self.curSize = list(size); + self.width, self.height = self.GetClientSize() + self.videoPath = videoPath + # }}} + +# vim:expandtab foldmethod=marker sw=4 ts=4 tw=120 diff --git a/ENNToolMiRCARTColours.py b/ENNToolMiRCARTColours.py new file mode 100644 index 0000000..d06c9be --- /dev/null +++ b/ENNToolMiRCARTColours.py @@ -0,0 +1,27 @@ +#!/usr/bin/env python3 +# +# ENNTool -- mIRC art animation tool (for EFnet #MiRCART) (WIP) +# Copyright (c) 2018 Lucio Andrés Illanes Albornoz +# This project is licensed under the terms of the MIT license. +# + +ENNToolMiRCARTColours = [ + [1.00, 1.00, 1.00], # White + [0.00, 0.00, 0.00], # Black + [0.00, 0.00, 0.73], # Blue + [0.00, 0.73, 0.00], # Green + [1.00, 0.33, 0.33], # Light Red + [0.73, 0.00, 0.00], # Red + [0.73, 0.00, 0.73], # Purple + [0.73, 0.73, 0.00], # Yellow + [1.00, 1.00, 0.33], # Light Yellow + [0.33, 1.00, 0.33], # Light Green + [0.00, 0.73, 0.73], # Cyan + [0.33, 1.00, 1.00], # Light Cyan + [0.33, 0.33, 1.00], # Light Blue + [1.00, 0.33, 1.00], # Pink + [0.33, 0.33, 0.33], # Grey + [0.73, 0.73, 0.73], # Light Grey +] + +# vim:expandtab foldmethod=marker sw=4 ts=4 tw=120 diff --git a/ENNToolMiRCARTImporter.py b/ENNToolMiRCARTImporter.py new file mode 100644 index 0000000..8bd1893 --- /dev/null +++ b/ENNToolMiRCARTImporter.py @@ -0,0 +1,135 @@ +#!/usr/bin/env python3 +# +# ENNTool -- mIRC art animation tool (for EFnet #MiRCART) (WIP) +# Copyright (c) 2018 Lucio Andrés Illanes Albornoz +# This project is licensed under the terms of the MIT license. +# +# TODO: +# 1) un-Quick'n'Dirty-ify +# + +import chardet + +class ENNToolMiRCARTImporter(object): + """XXX""" + + # {{{ _CellState(): Cell state + class _CellState(): + CS_NONE = 0x00 + CS_BOLD = 0x01 + CS_ITALIC = 0x02 + CS_UNDERLINE = 0x04 + # }}} + # {{{ _ParseState(): Parsing loop state + class _ParseState(): + PS_CHAR = 1 + PS_COLOUR_DIGIT0 = 2 + PS_COLOUR_DIGIT1 = 3 + # }}} + + # {{{ _flipCellStateBit(self, cellState, bit): XXX + def _flipCellStateBit(self, cellState, bit): + if cellState & bit: + return cellState & ~bit + else: + return cellState | bit + # }}} + # {{{ _parseCharAsColourSpec(self, colourSpec, curColours): XXX + def _parseCharAsColourSpec(self, colourSpec, curColours): + if len(colourSpec) > 0: + colourSpec = colourSpec.split(",") + if len(colourSpec) == 2 \ + and len(colourSpec[1]) > 0: + return [int(colourSpec[0] or curColours[0]), \ + int(colourSpec[1])] + elif len(colourSpec) == 1 \ + or len(colourSpec[1]) == 0: + return [int(colourSpec[0]), curColours[1]] + else: + return [15, 1] + # }}} + # {{{ fromTextFile(self, pathName): XXX + def fromTextFile(self, pathName): + with open(pathName, "rb") as fileObject: + inFileEncoding = chardet.detect(fileObject.read())["encoding"] + self.inFile = open(pathName, "r", encoding=inFileEncoding) + self.inSize = self.outMap = None; + inCurColourSpec = ""; inCurRow = -1; + inLine = self.inFile.readline() + inSize = [0, 0]; outMap = []; inMaxCols = 0; + while inLine: + inCellState = self._CellState.CS_NONE + inParseState = self._ParseState.PS_CHAR + inCurCol = 0; inMaxCol = len(inLine); + inCurColourDigits = 0; inCurColours = [15, 1]; inCurColourSpec = ""; + inCurRow += 1; outMap.append([]); inRowCols = 0; inSize[1] += 1; + while inCurCol < inMaxCol: + inChar = inLine[inCurCol] + if inChar in set("\r\n"): \ + inCurCol += 1 + elif inParseState == self._ParseState.PS_CHAR: + inCurCol += 1 + if inChar == "": + inCellState = self._flipCellStateBit( \ + inCellState, self._CellState.CS_BOLD) + elif inChar == "": + inParseState = self._ParseState.PS_COLOUR_DIGIT0 + elif inChar == "": + inCellState = self._flipCellStateBit( \ + inCellState, self._CellState.CS_ITALIC) + elif inChar == "": + inCellState |= self._CellState.CS_NONE + inCurColours = [15, 1] + elif inChar == "": + inCurColours = [inCurColours[1], inCurColours[0]] + elif inChar == "": + inCellState = self._flipCellStateBit( \ + inCellState, self._CellState.CS_UNDERLINE) + else: + inRowCols += 1 + outMap[inCurRow].append([*inCurColours, inCellState, inChar]) + elif inParseState == self._ParseState.PS_COLOUR_DIGIT0 \ + or inParseState == self._ParseState.PS_COLOUR_DIGIT1: + if inChar == "," \ + and inParseState == self._ParseState.PS_COLOUR_DIGIT0: + if (inCurCol + 1) < inMaxCol \ + and not inLine[inCurCol + 1] in set("0123456789"): + inCurColours = self._parseCharAsColourSpec( \ + inCurColourSpec, inCurColours) + inCurColourDigits = 0; inCurColourSpec = ""; + inParseState = self._ParseState.PS_CHAR + else: + inCurCol += 1 + inCurColourDigits = 0; inCurColourSpec += inChar; + inParseState = self._ParseState.PS_COLOUR_DIGIT1 + elif inChar in set("0123456789") \ + and inCurColourDigits == 0: + inCurCol += 1 + inCurColourDigits += 1; inCurColourSpec += inChar; + elif inChar in set("0123456789") \ + and inCurColourDigits == 1 \ + and inCurColourSpec[-1] == "0": + inCurCol += 1 + inCurColourDigits += 1; inCurColourSpec += inChar; + elif inChar in set("012345") \ + and inCurColourDigits == 1 \ + and inCurColourSpec[-1] == "1": + inCurCol += 1 + inCurColourDigits += 1; inCurColourSpec += inChar; + else: + inCurColours = self._parseCharAsColourSpec( \ + inCurColourSpec, inCurColours) + inCurColourDigits = 0; inCurColourSpec = ""; + inParseState = self._ParseState.PS_CHAR + inMaxCols = max(inMaxCols, inRowCols) + inLine = self.inFile.readline() + inSize[0] = inMaxCols; self.inSize = inSize; self.outMap = outMap; + self.inFile.close() + # }}} + # {{{ __init__(self, inFile): initialisation method + def __init__(self, inFile): + self.inFile = inFile; self.inSize = self.outMap = None; + self.fromTextFile(inFile) + # }}} + +# vim:expandtab foldmethod=marker sw=4 ts=4 tw=120 diff --git a/LICENCE b/LICENCE new file mode 100644 index 0000000..7381187 --- /dev/null +++ b/LICENCE @@ -0,0 +1,19 @@ +Copyright (c) 2018 Lucio Andrés Illanes Albornoz + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/README.md b/README.md new file mode 100644 index 0000000..9446120 --- /dev/null +++ b/README.md @@ -0,0 +1,5 @@ +# ENNTool -- mIRC art animation tool (for EFnet #MiRCART) (WIP) +Copyright (c) 2018 Lucio Andrés Illanes Albornoz <> +This project is licensed under the terms of the MIT licence. +* Prerequisites on Windows: install Python v3.5.x and script dependencies w/ the following elevated command prompt command line: + `pip install chardet numpy opencv-python Pillow PyOpenGL wxPython` diff --git a/puke.txt b/puke.txt new file mode 100644 index 0000000..1d22a35 --- /dev/null +++ b/puke.txt @@ -0,0 +1,36 @@ +0,1 3,3 0,1 9,9 0,1 9,9 0,1 9,9 0,1 9,9 0,1 9,9 0,1 9,9 0,1 9,9 0,1 3,3 0,1 0,0 1,1 0,1 0,0 0,1 9,9 0,1 9,9 0,1 9,9 0,1 9,9 0,1 9,9 0,1 9,9 0,1 9,9 0,1 9,9 0,1 9,9 0,1 9,9 1,1 9,9 1,1 9,9 1,1 0,1 9,9 0,1 9,9 0,1 9,9 0,1 9,9 0,1 9,9 0,1 +3,3 0,1 3,3 0,1 9,9 0,1 9,9 1,1 9,9 0,1 9,9 0,1 9,9 0,1 9,9 0,1 9,9 0,1 9,9 0,1 0,0 0,1 0,0 0,1 0,0 0,1 9,9 0,1 9,9 0,1 9,9 0,1 9,9 0,1 9,9 0,1 9,9 0,1 1,1 9,9 0,1 9,9 1,1 0,1 9,9 0,1 9,9 0,1 9,9 0,1 9,9 0,1 +0,1 3,3 0,1 9,9 0,1 11,11 10,10 11,11 0,1 9,9 0,1 9,9 0,1 3,3 0,1 3,3 0,1 0,0 0,1 0,0 0,1 0,0 0,1 9,9 0,1 9,9 0,1 9,9 0,1 1,1 9,9 0,1 1,1 0,1 9,9 0,1 9,9 0,1 +0,1 3,3 0,1 3,3 0,1 10,10 11,11 10,10 11,11 10,10 0,1 3,3 0,1 3,3 0,1 0,0 0,1 3,3 0,1 3,3 0,1 1,1 3,3 1,1 3,3 1,1 9,9 1,1 9,9 1,1 3,3 1,1 3,3 1,1 0,1 3,3 0,1 3,3 0,1 +9,9 1,1 3,3 1,1 10,10 11,11 10,10 11,11 10,10 1,1 0,0 1,1 11,11 1,10 1,1 11,11 1,1 3,3 1,1 9,9 1,1 3,3 1,1 11,11 1,1 1,10 11,11 0,1 9,9 +9,9 1,1 10,10 11,11 10,10 11,11 10,10 11,11 1,1 3,3 1,1 11,11 1,10 1,1 11,11 1,1 3,3 1,1 3,3 1,1 11,11 1,1 1,10 11,11 0,1 9,9 +9,9 1,1 10,10 11,11 10,10 11,11 10,10 1,1 8,8 1,1 11,11 1,10 1,1 11,11 1,1 3,3 1,1 11,11 1,1 1,10 11,11 1,1 0,1 9,9 +9,9 1,1 11,11 10,10 11,11 10,10 11,11 10,10 11,11 10,10 1,1 8,8 1,1 8,8 1,1 11,11 1,10 1,1 11,11 1,1 1,10 11,11 1,1 0,1 9,9 +9,9 1,1 11,11 10,10 11,11 10,10 11,10 10,10 11,11 10,10 1,1 8,8 1,1 8,8 1,1 11,11 1,10 1,1 1,10 11,11 1,1 0,1 9,9 +9,9 1,1 11,11 10,10 11,11 10,10 11,11 10,10 1,1 8,8 1,1 8,8 1,1 8,8 1,1 0,0 1,1 8,8 1,1 8,8 1,1 8,8 1,1 11,11 1,10 11,11 1,10 11,11 1,1 0,1 9,9 +1,3 9,9 1,1 11,11 10,10 11,11 10,10 11,11 1,1 8,8 1,1 8,8 1,1 8,8 1,1 0,0 1,1 0,0 1,1 8,8 1,1 8,8 1,1 8,8 1,1 11,11 10,10 11,11 1,1 0,1 9,9 1,3 +3,3 9,9 1,1 3,3 1,1 3,3 1,1 11,11 10,10 1,1 8,8 1,1 8,8 1,1 8,8 1,1 0,0 1,1 8,8 1,1 8,8 1,1 8,8 1,1 3,3 9,9 3,3 1,1 0,1 9,9 1,3 +3,3 9,9 1,1 3,3 1,1 3,3 1,1 3,3 1,1 3,3 1,1 8,8 1,1 8,8 1,1 8,8 1,1 0,0 1,1 8,8 1,1 8,8 1,1 0,0 1,1 8,8 1,1 8,8 1,1 8,8 1,1 3,3 9,9 1,1 0,1 9,9 1,3 +3,3 9,9 1,1 3,3 1,1 3,3 1,1 3,3 1,1 3,3 1,1 8,8 1,1 8,8 1,1 8,8 1,1 0,0 1,1 0,0 1,1 8,8 1,1 8,8 1,1 0,0 1,1 0,0 1,1 8,8 1,1 8,8 1,1 8,8 1,1 3,3 9,9 3,3 1,1 0,1 9,9 1,3 +3,3 9,9 1,1 9,9 1,1 9,9 1,1 9,9 1,1 7,7 8,8 1,1 8,8 1,1 8,8 1,1 0,0 1,1 8,8 1,1 0,0 1,1 8,8 1,1 8,8 1,1 8,8 1,1 9,9 3,3 1,1 0,1 9,9 1,3 +1,3 3,3 9,9 1,1 9,9 1,1 9,9 1,1 9,9 1,1 9,9 1,1 9,9 1,1 9,9 1,1 7,7 8,8 1,1 8,8 1,1 8,8 1,1 3,3 9,9 3,3 1,1 0,1 9,9 1,3 +11,11 3,3 9,9 1,1 7,7 8,8 1,1 8,8 1,1 8,8 1,1 8,8 1,1 3,3 9,9 1,1 0,1 9,9 1,3 11,11 +11,11 3,3 9,9 1,1 3,3 1,1 3,3 1,1 3,3 1,1 3,3 1,1 3,3 1,1 3,3 1,1 7,7 8,8 1,1 0,0 1,1 8,8 1,1 3,3 9,9 3,3 1,1 0,1 9,9 1,3 11,11 +11,11 1,3 3,3 9,9 1,1 9,9 1,1 9,9 1,1 9,9 1,1 9,9 1,1 9,9 1,1 7,7 8,8 1,1 8,8 1,1 9,9 3,3 1,1 0,1 9,9 1,3 11,11 +10,10 3,11 1,3 3,3 9,9 1,1 3,3 1,1 3,3 1,1 7,7 8,8 1,1 0,0 1,1 0,0 1,1 0,0 1,1 8,8 1,1 3,3 1,1 9,9 1,3 3,11 10,10 +10,10 3,11 1,3 3,3 9,9 1,1 3,3 1,1 3,3 1,1 3,3 1,1 3,3 1,1 3,3 1,1 7,7 8,8 8,1 1,1 8,1 8,8 1,1 9,9 3,3 1,1 9,9 1,3 3,11 10,10 +10,10 3,11 1,3 3,3 9,9 1,1 9,9 1,1 9,9 1,1 9,9 1,1 9,9 1,1 7,7 8,8 8,1 0,0 1,1 0,0 1,1 0,0 8,1 8,8 1,1 3,3 9,9 1,3 3,11 10,10 +10,10 3,11 3,3 9,9 7,7 8,8 8,1 1,1 8,1 8,8 7,7 9,9 3,3 1,3 3,11 10,10 +10,10 11,11 3,3 7,7 8,8 8,1 0,0 8,1 8,8 7,7 3,3 11,11 10,10 +10,10 11,11 3,3 9,9 7,7 8,8 8,1 8,8 7,7 9,9 3,3 11,11 10,10 +10,10 11,11 3,3 9,9 3,3 7,7 3,3 9,9 3,3 11,11 10,10 +10,10 11,11 3,3 9,9 3,3 11,11 3,3 9,9 3,3 11,11 10,10 +11,11 3,3 9,9 3,3 11,11 10,10 11,11 3,3 9,9 3,3 11,11 +11,11 3,3 9,9 3,3 11,11 10,10 11,11 10,10 11,11 3,3 9,9 3,3 11,11 +11,11 3,3 9,9 3,3 11,11 10,10 11,11 10,10 11,11 10,10 11,11 3,3 9,9 3,3 11,11 +11,11 3,3 9,9 3,3 11,11 10,10 11,11 10,10 11,11 3,3 9,9 3,3 11,11 +10,10 11,11 3,3 9,9 3,3 11,11 10,10 11,11 3,3 9,9 3,3 11,11 10,10 +10,10 11,11 3,3 9,9 3,3 11,11 3,3 9,9 3,3 11,11 10,10 +10,10 11,11 3,3 9,9 3,3 9,9 3,3 11,11 10,10 +10,10 11,11 3,3 9,9 3,3 11,11 10,10 +10,10 11,11 3,3 11,11 10,10 diff --git a/texture.png b/texture.png new file mode 100644 index 0000000..ccdcd52 Binary files /dev/null and b/texture.png differ