やってみる

アウトプットすべく己を導くためのブログ。その試行錯誤すらたれ流す。

GIF,APNG,WebPアニメを出力する

 ドット絵ならアニメーションでしょ。

成果物

 出力結果は以下。

type result
gif animation.gif
png animation.png
webp animation.webp

コード

コード(520行) main.py

#!/usr/bin/python3
# -*- coding: utf-8 -*-
import sys, os, numpy, PIL
from PySide2 import QtCore, QtGui, QtWidgets
from PIL import Image, ImagePalette

class Window(QtWidgets.QMainWindow):
    def __init__(self):
        super(self.__class__, self).__init__()
        self.setAcceptDrops(True)
        self.widget = Widget(self)
        self.setCentralWidget(self.widget)

        menu_file = QtWidgets.QMenu('File', self)
        menu_file.addAction(self.widget.GraphicsView.Scene.Drawable.SaveAction)
        self.menuBar().addMenu(menu_file)

        menu_frame = QtWidgets.QMenu('Animation', self)
        menu_frame.addAction(FrameListView.AddFrameAction)
        menu_frame.addAction(FrameListView.DeleteFrameAction)
        self.menuBar().addMenu(menu_frame)

        self.show()

        # Frame側でも使いたいので
        globals()['Window'] = self

    def mousePressEvent(self, event):
        super(self.__class__, self).mousePressEvent(event)
        self.widget.update()
    def mouseMoveEvent(self, event):
        super(self.__class__, self).mouseMoveEvent(event)
        self.widget.update()
    def dragEnterEvent(self, event):
        super(self.__class__, self).dragEnterEvent(event)
        self.widget.update()
    def dragMoveEvent(self, event):
        super(self.__class__, self).dragMoveEvent(event)
    def dropEvent(self, event):
        super(self.__class__, self).dropEvent(event)

class Widget(QtWidgets.QWidget):
    def __init__(self, parent):
        super(self.__class__, self).__init__(parent)
        self.setAcceptDrops(True)
        self.view = GraphicView()
        self.framelist = FrameListView()

        # DrawableItemで使いたいので
        globals()['FrameListView'] = self.framelist

        scroller1 = QtWidgets.QScrollArea()
        scroller1.setWidget(self.view)

        scroller2 = QtWidgets.QScrollArea()
        scroller2.setWidget(self.framelist)

        layout = QtWidgets.QGridLayout()
        layout.addWidget(scroller1, 0, 0)
        layout.addWidget(scroller2, 0, 1)

        self.setLayout(layout)
        self.resize(self.view.width(), self.view.height())
        self.setWindowTitle("QAction")
        self.show()
    @property
    def GraphicsView(self): return self.view
    def mousePressEvent(self, event):
        super(self.__class__, self).mousePressEvent(event)
        self.view.scene().update()
        self.view.update()
    def mouseMoveEvent(self, event):
        super(self.__class__, self).mouseMoveEvent(event)
        self.view.scene().update()
        self.view.update()
    def dragEnterEvent(self, event):
        super(self.__class__, self).dragEnterEvent(event)
        self.view.dragEnterEvent(event)
        self.view.scene().update()
        self.view.update()
    def dragMoveEvent(self, event):
        super(self.__class__, self).dragMoveEvent(event)
    def dropEvent(self, event):
        super(self.__class__, self).dropEvent(event)

class GraphicView(QtWidgets.QGraphicsView):
    def __init__(self):
        QtWidgets.QGraphicsView.__init__(self)
        self.setAcceptDrops(True)
        self.setWindowTitle("QGraphicsScene draw Grid")
        self.__editorScene = EditorScene(self)
        self.setScene(self.__editorScene)
    def mousePressEvent(self, event):
        super(self.__class__, self).mousePressEvent(event)
        self.scene().update()
    def mouseMoveEvent(self, event):
        super(self.__class__, self).mouseMoveEvent(event)
        self.scene().update()
    @property
    def Scene(self): return self.__editorScene
    def dragEnterEvent(self, event):
        super(self.__class__, self).dragEnterEvent(event)
        self.scene().update()
    def dragEnterEvent(self, event):
        super(self.__class__, self).dragMoveEvent(event)
        self.scene().update()
    def dropEvent(self, event):
        super(self.__class__, self).dropEvent(event)
        self.scene().update()

class EditorScene(QtWidgets.QGraphicsScene):
    def __init__(self, *args, **kwargs):
        super(self.__class__, self).__init__(*args, **kwargs)
        self.size = 16
        self.scale = 32
        self.setSceneRect(0, 0, self.size*self.scale, self.size*self.scale)

        self.grid = GridItem()
        self.addItem(self.grid)

        self.background = BackgroundItem()
        self.addItem(self.background)

        self.drawable = DrawableItem()
        self.addItem(self.drawable)

        self.background.setZValue(0)
        self.drawable.setZValue(1)
        self.grid.setZValue(9999)

        # Frame側でも使いたいので
        globals()['Drawable'] = self.drawable

    def mousePressEvent(self, event):
        for item in self.items():
            item.mousePressEvent(event)
        super(self.__class__, self).mousePressEvent(event)
    def mouseMoveEvent(self, event):
        for item in self.items():
            item.setAcceptHoverEvents(True)
            item.mouseMoveEvent(event)
        super(self.__class__, self).mouseMoveEvent(event)
    def dragEnterEvent(self, event):
        for item in self.items():
            item.setAcceptDrops(True)
            if event is type(QtWidgets.QGraphicsSceneDragDropEvent):
                item.dragEnterEvent(event)
        if event is type(QtWidgets.QGraphicsSceneDragDropEvent):
            super(self.__class__, self).dragEnterEvent(event)
    def dragMoveEvent(self, event):
        for item in self.items():
            item.setAcceptDrops(True)
            if event is type(QtWidgets.QGraphicsSceneDragDropEvent):
                item.dragEnterEvent(event)
        if event is type(QtWidgets.QGraphicsSceneDragDropEvent):
            super(self.__class__, self).dragEnterEvent(event)
    def dropEvent(self, event):
        for item in self.items():
            item.setAcceptDrops(True)
            item.dropEvent(event)
        if event is type(QtWidgets.QGraphicsSceneDragDropEvent):
            super(self.__class__, self).dropEvent(event)
    @property
    def Grid(self): return self.grid
    @property
    def Background(self): return self.background
    @property
    def Drawable(self): return self.drawable

class DrawableItem(QtWidgets.QGraphicsRectItem):
    def __init__(self, *args, **kwargs):
        super(self.__class__, self).__init__(*args, **kwargs)
        self.setAcceptDrops(True)
        self.setAcceptHoverEvents(True)
        self.scale = 32
        self.pixels = Pixels()
        self.actions = {}
        self.__create_save_action()

    def __create_save_action(self):
        a = QtWidgets.QAction('Save')
        a.setObjectName('Save')
        a.setShortcut('Ctrl+S')
        a.triggered.connect(self.Pixels.save)
        self.actions['Save'] = a

    def paint(self, painter, option, widget):
        painter.fillRect(widget.rect(), QtGui.QBrush( QtGui.QColor(0,0,0,0), QtCore.Qt.SolidPattern))
        for y in range(self.pixels.Height):
            for x in range(self.pixels.Width):
                if 1 == self.pixels.Pixels[y][x]:
                    painter.fillRect(x*self.scale, y*self.scale, self.scale, self.scale, QtGui.QBrush( QtGui.QColor(255,0,0,128), QtCore.Qt.SolidPattern))
 
    def mouseMoveEvent(self, event):
        pos = event.scenePos()
        x = int(pos.x()//self.scale)
        y = int(pos.y()//self.scale)
        if event.buttons() & QtCore.Qt.LeftButton:
            self.pixels.Pixels[y][x] = 1
#            print(x, y, self.pixels.Pixels)
            for idx in FrameListView.selectedIndexes():
                FrameListModel.Frames[idx.row()].Pixels.Pixels[y][x] = 1
                FrameListModel.update_icon(idx)
#                print(idx.row(), 'Drawable', x, y, FrameListModel.Frames[idx.row()].Pixels.Pixels)
#                FrameListView.mouseMoveEvent(QtGui.QMouseEvent(QtCore.QEvent.MouseButtonPress, event.pos(), event.button(), event.buttons(), QtCore.Qt.NoModifier))
#                FrameListView.mouseMoveEvent(QtGui.QMouseEvent(QtCore.QEvent.MouseMove, event.pos(), event.button(), event.buttons(), QtCore.Qt.NoModifier))
#            FrameListView.mouseMoveEvent(QtGui.QMouseEvent(QtCore.QEvent.MouseMove, event.pos(), event.button(), event.buttons(), QtCore.Qt.NoModifier))
#            FrameListView.update_icon()
#            FrameListView.update()
#            FrameListView.repaint()
            # 再描画。意味不明だがListViewのマウスイベントを発行すればListViewが再描画されることを発見した。なぜかupdate()やrepaint()では一切再描画されない。
            # だがListViewの先頭項目が選択されてしまう。このせいでバグるため再描画させられない。
#            super(FrameListView.__class__, FrameListView).mousePressEvent(QtGui.QMouseEvent(QtCore.QEvent.MouseMove, event.pos(), event.button(), event.buttons(), QtCore.Qt.NoModifier))
#            FrameListView.mousePressEvent(QtGui.QMouseEvent(QtCore.QEvent.MouseMove, event.pos(), event.button(), event.buttons(), QtCore.Qt.NoModifier))
            """
            FrameListView.update()
            FrameListView.updateGeometry()
            FrameListView.repaint()
            Window.update()
            Window.repaint()
            """
            FrameListView.update()
            Window.update()
        if event.buttons() & QtCore.Qt.RightButton:
            self.pixels.Pixels[y][x] = 0
            for idx in FrameListView.selectedIndexes():
                FrameListModel.Frames[idx.row()].Pixels.Pixels[y][x] = 0
                FrameListModel.update_icon(idx)
                print(idx.row(), 'Drawable', x, y, FrameListModel.Frames[idx.row()].Pixels.Pixels)
#            super(FrameListView.__class__, FrameListView).mousePressEvent(QtGui.QMouseEvent(QtCore.QEvent.MouseMove, event.pos(), event.button(), event.buttons(), QtCore.Qt.NoModifier))
#            FrameListView.mousePressEvent(QtGui.QMouseEvent(QtCore.QEvent.MouseMove, event.pos(), event.button(), event.buttons(), QtCore.Qt.NoModifier))
            """
            FrameListView.update()
            FrameListView.updateGeometry()
            FrameListView.repaint()
            Window.update()
            Window.repaint()
            """

    def mousePressEvent(self, event):
        pos = event.scenePos()
        x = int(pos.x()//self.scale)
        y = int(pos.y()//self.scale)
        if event.buttons() & QtCore.Qt.LeftButton:
            self.pixels.Pixels[y][x] = 1
            for idx in FrameListView.selectedIndexes():
                FrameListModel.Frames[idx.row()].Pixels.Pixels[y][x] = 1
                FrameListModel.update_icon(idx)
                print(idx.row(), 'Drawable', x, y, FrameListModel.Frames[idx.row()].Pixels.Pixels)
#            super(FrameListView.__class__, FrameListView).mousePressEvent(QtGui.QMouseEvent(QtCore.QEvent.MouseMove, event.pos(), event.button(), event.buttons(), QtCore.Qt.NoModifier))
#            FrameListView.mousePressEvent(QtGui.QMouseEvent(QtCore.QEvent.MouseMove, event.pos(), event.button(), event.buttons(), QtCore.Qt.NoModifier))
            """
            FrameListView.update()
            FrameListView.updateGeometry()
            FrameListView.repaint()
            Window.update()
            Window.repaint()
            """

        if event.buttons() & QtCore.Qt.RightButton:
            self.pixels.Pixels[y][x] = 0
            for idx in FrameListView.selectedIndexes():
                FrameListModel.Frames[idx.row()].Pixels.Pixels[y][x] = 0
                FrameListModel.update_icon(idx)
                print(idx.row(), 'Drawable', x, y, FrameListModel.Frames[idx.row()].Pixels.Pixels)
#            super(FrameListView.__class__, FrameListView).mousePressEvent(QtGui.QMouseEvent(QtCore.QEvent.MouseMove, event.pos(), event.button(), event.buttons(), QtCore.Qt.NoModifier))
#            FrameListView.mousePressEvent(QtGui.QMouseEvent(QtCore.QEvent.MouseMove, event.pos(), event.button(), event.buttons(), QtCore.Qt.NoModifier))
            """
            FrameListView.update()
            FrameListView.updateGeometry()
            FrameListView.repaint()
            Window.update()
            Window.repaint()
            Window.widget.update()
            Window.widget.repaint()
            """

    def mouseReleaseEvent(self, event):
        pass
    def mouseDoubleClickEvent(self, event):
        pass
    @property
    def Pixels(self): return self.pixels
    @Pixels.setter
    def Pixels(self, value):
        #self.pixels = value
        for y in range(value.Height):
            for x in range(value.Width):
                self.pixels.Pixels[y][x] = value.Pixels[y][x]
    @property
    def SaveAction(self): return self.actions['Save']
    def dragEnterEvent(self, event):
        if event.mimeData().hasUrls():
            event.acceptProposedAction()
    def dropEvent(self, event):
        for url in event.mimeData().urls():
            file_name = url.toLocalFile()
            print("Dropped file: " + file_name)
            self.Pixels.load(file_name)

class BackgroundItem(QtWidgets.QGraphicsRectItem):
    def __init__(self, *args, **kwargs):
        super(self.__class__, self).__init__(*args, **kwargs)
        self.size = 16
        self.scale = 32
        self.colors = [QtGui.QColor(196,196,196,255), QtGui.QColor(232,232,232,255)]
    def paint(self, painter, option, widget):
        for i in range(self.size*self.size):
            x = (i % self.size)
            y = (i // self.size)
            color = QtGui.QColor(128,128,128,255) if 0 == (i % 2) and 0 == (x % 2) else QtGui.QColor(196,196,196,255)
            painter.fillRect(x * (self.scale),               y * (self.scale),               self.scale//2, self.scale//2, self.colors[0])
            painter.fillRect(x * (self.scale)+self.scale//2, y * (self.scale)+self.scale//2, self.scale//2, self.scale//2, self.colors[0])
            painter.fillRect(x * (self.scale)+self.scale//2, y * (self.scale),               self.scale//2, self.scale//2, self.colors[1])
            painter.fillRect(x * (self.scale),               y * (self.scale)+self.scale//2, self.scale//2, self.scale//2, self.colors[1])

class GridItem(QtWidgets.QGraphicsRectItem):
    def __init__(self, *args, **kwargs):
        super(self.__class__, self).__init__(*args, **kwargs)
        self.size = 16
        self.scale = 32
    def paint(self, painter, option, widget):
        painter.fillRect(widget.rect(), QtGui.QBrush(QtGui.QColor(0,0,0,0), QtCore.Qt.SolidPattern))
        lines = []
        for y in range(self.size+1):
            lines.append(QtCore.QLine(0, y*self.scale, self.size*self.scale, y*self.scale))
        for x in range(self.size+1):
            lines.append(QtCore.QLine(x*self.scale, 0, x*self.scale, self.size*self.scale))
        painter.drawLines(lines)

class Pixels:
    def __init__(self):
        self.width = 16
        self.height = 16
        self.pixels = numpy.zeros(self.width*self.height, dtype=int).reshape(self.height, self.width)
    @property
    def Pixels(self): return self.pixels
    @property
    def Width(self): return self.width
    @property
    def Height(self): return self.height
    def save(self):
        print(os.getcwd())
        self.save_txt()
        for ext in ('gif', 'png', 'webp'):
            self.save_raster(ext)
        for ext in ('gif', 'png', 'webp'):
            self.save_animation(ext)
    def load(self, file_path):
        ext = os.path.splitext(file_path)[1].lower()[1:]
        if '' == ext: raise Exception('拡張子が必要です。png,gif,webp,txt形式のいずれかに対応しています。')
        elif 'txt' == ext: self.load_txt(file_path)
        elif 'gif' == ext: self.load_gif(file_path)
        elif 'png' == ext: self.load_png(file_path)
        elif 'webp' == ext: self.load_webp(file_path)
        else: raise Exception('拡張子が未対応です。png,gif,webp,txt形式のいずれかに対応しています。')
    def save_txt(self):
        with open(os.path.join(os.getcwd(), 'pixels.txt'), 'w') as f:
            f.write('\n'.join([''.join(map(str, self.pixels[y].tolist())) for y in range(self.height)]))
    def load_txt(self, file_path):
        with open(file_path, 'r') as f:
            lines = f.read().split('\n')
            self.height = len(lines)
            self.width = len(lines[0])
            self.pixels = numpy.zeros(self.width*self.height, dtype=int).reshape(self.height, self.width)
            x = 0; y = 0;
            for line in lines:
                for c in line:
                    self.pixels[y][x] = int(c, 16)
                    x += 1
                y += 1
                x = 0

    def save_raster(self, ext):
        image = Image.new('1', (self.width, self.height))
        image.putdata(self.pixels.reshape(self.width * self.height).tolist())
        print(ext)
        image.save(os.path.join(os.getcwd(), 'pixels.' + ext), optimize=True, lossless=True)

    def save_animation(self, ext):
        print(ext)
        if len(FrameListView.Model.Frames) < 2: return
        images = []
        for frame in FrameListView.Model.Frames:
            image = Image.new('P', (frame.Pixels.Width, frame.Pixels.Height))
            image.putpalette([0,0,0,255,255,255])
            image.putdata(frame.Pixels.Pixels.reshape(frame.Pixels.Width * frame.Pixels.Height).tolist())
            images.append(image)
        image.save(os.path.join(os.getcwd(), 'animation.' + ext), save_all=True, append_images=images, duration=100, loop=0, optimize=False)
#        image.save(os.path.join(os.getcwd(), 'pixels.' + ext), optimize=True, lossless=True, save_all=True, append_images=images)

    def load_png(self, file_path):
        image = Image.open(file_path, mode='r')
        image = image.convert('1')
        print(len(image.getdata()), list(image.getdata()))
        self.pixels = numpy.array(list(map(lambda x: 0 if 0 == x else 1, list(image.getdata())))).reshape(image.size[1], image.size[0])
        self.width, self.height = image.size
        print(self.width, self.height)
    def load_gif(self, file_path): # 値が0/255で出力されてしまうので0/1に変換する
        image = Image.open(file_path, mode='r')
        self.width, self.height = image.size
        self.pixels = numpy.array(list(map(lambda x: 0 if 0 == x else 1, list(image.getdata())))).reshape(self.height, self.width)
    def load_webp(self, file_path): # 値が[0,0,0]/[255,255,255]で出力されてしまうので0/1に変換する
        image = Image.open(file_path, mode='r')
        self.width, self.height = image.size
        self.pixels = numpy.array(list(map(lambda x: 0 if (0,0,0) == x else 1, list(image.getdata())))).reshape(self.height, self.width)

class FrameListView(QtWidgets.QListView):
    def __init__(self, parent=None):
        super(self.__class__, self).__init__(parent)
        self.resizeContents(16, 16*16)
        self.model = FrameListModel()
        self.model.appendRow()
        self.setModel(self.model)
        globals()['FrameListModel'] = self.model
        self.resize(64, 16*32)
        self.actions = {}
        self.__create_add_frame_action()
        self.__create_delete_frame_action()
        self.setCurrentIndex(self.model.index(0,0))
        self.show()
    def mouseMoveEvent(self, event):
        super(self.__class__, self).mousePressEvent(event)
    def mousePressEvent(self, event):
        super(self.__class__, self).mousePressEvent(event)
        for idx in self.selectedIndexes():
            frame = idx.data(QtCore.Qt.UserRole)
            Drawable.Pixels = frame.Pixels
            Window.widget.view.scene().update()
    def update_icon(self):
        for idx in self.selectedIndexes():
            self.model.update_icon(idx)
        self.update()
        self.repaint()
    @property
    def Model(self): return self.model
    @property
    def AddFrameAction(self): return self.actions['AddFrame']
    @property
    def DeleteFrameAction(self): return self.actions['DeleteFrame']
    def __create_add_frame_action(self):
        a = QtWidgets.QAction('Add frame')
        a.setObjectName('AddFrame')
        a.setShortcut('Ctrl+Alt+N')
        a.triggered.connect(self.model.appendRow)
        self.actions['AddFrame'] = a
    def __create_delete_frame_action(self):
        a = QtWidgets.QAction('Delete frame')
        a.setObjectName('DeleteFrame')
        a.setShortcut('Ctrl+Alt+D')
        a.triggered.connect(self.__delete_frame)
        self.actions['DeleteFrame'] = a
    def __delete_frame(self):
        if len(self.model.Frames) < 2: return
        for idx in self.selectedIndexes():
            if idx.row() == self.model.rowCount()-1:
                self.setCurrentIndex(self.model.index(idx.row()-1,0))
            else:
                self.setCurrentIndex(self.model.index(idx.row(),0))
            self.model.removeRow(idx)
        for idx in self.selectedIndexes():
            frame = idx.data(QtCore.Qt.UserRole)
            Drawable.Pixels = frame.Pixels
            Window.widget.view.scene().update()

#        self.update()

class FrameListModel(QtCore.QAbstractListModel):
    def __init__(self, parent=None):
        super(self.__class__, self).__init__(parent)
        self.frames = []
    def rowCount(self, parent=QtCore.QModelIndex()):
        if parent.isValid(): return 0
        return len(self.frames)
    def data(self, index, role=QtCore.Qt.DisplayRole):
        if role == QtCore.Qt.DecorationRole:
            return self.frames[index.row()].Icon
        elif  role == QtCore.Qt.UserRole:
            return self.frames[index.row()]
    def appendRow(self):
        self.beginInsertRows(QtCore.QModelIndex(), self.rowCount(), self.rowCount())
        self.frames.append(Frame())
        self.endInsertRows()
    def removeRow(self, index):
        self.beginRemoveRows(QtCore.QModelIndex(), self.rowCount(), self.rowCount())
        print(index.row())
        self.frames.pop(index.row())
        self.endRemoveRows()
    def update_icon(self, index):
        self.frames[index.row()].update_icon()
    @property
    def Frames(self): return self.frames

class Frame:
    def __init__(self):
        self.pixels = Pixels()
        self.icon = QtGui.QImage(self.pixels.Width, self.pixels.Height, QtGui.QImage.Format_Mono)
        self.update_icon()
    def update_icon(self):
        image = QtGui.QImage(self.pixels.Width, self.pixels.Height, QtGui.QImage.Format_Mono)
        for y in range(self.pixels.Height):
            for x in range(self.pixels.Width):
                image.setPixel(x, y, self.pixels.Pixels[y][x])
                if 1 < self.pixels.Pixels[y][x]: print(self.pixels.Pixels[y][x])
        self.icon = QtGui.QIcon(QtGui.QPixmap.fromImage(image))
    @property
    def Pixels(self): return self.pixels
    @Pixels.setter
    def Pixels(self, value): self.pixels = value
    @property
    def Icon(self): return self.icon
    @Icon.setter
    def Icon(self, value): self.icon = value


if __name__ == "__main__":
    app = QtWidgets.QApplication(sys.argv)
    window = Window()
    sys.exit(app.exec_())
python3 main.py

苦労した所

ListViewの項目削除

 QAbstractItemModelbeginRemoveRows()endRemoveRows()を使う。この作法を知らないと実装できない。

class FrameListModel(QtCore.QAbstractListModel):
    ...
    def removeRow(self, index):
        self.beginRemoveRows(QtCore.QModelIndex(), self.rowCount(), self.rowCount())
        self.frames.pop(index.row())
        self.endRemoveRows()

ListViewの項目選択

 最初に1番目の項目を選択した状態にしたかった。ListView.setCurrentIndex()を使う。

self.setCurrentIndex(self.model.index(0,0))

 項目を削除したとき、選択状態が解除され、何も選択されない状態になってしまった。これを解決するため、現在の選択位置ごとに応じて次にセットする位置を分岐してやる必要があった。

 また、先に削除すると何も選択されない状態になってしまう。よって先に項目選択してから削除した。

if idx.row() == self.model.rowCount()-1:
    self.setCurrentIndex(self.model.index(idx.row()-1,0))
else:
    self.setCurrentIndex(self.model.index(idx.row(),0))
self.model.removeRow(idx)

アニメ出力

 Pillowのバージョンが5と古かったため、APNG出力でエラーになった。以下で更新して7.1.2にすると成功した。

pip3 install -U pillow

 Pillowsave()を使う。save_all, append_images引数を渡す。

 ただ、mode1では真っ黒な画像しか出力されなかった。仕方なくPモードにして、2色のパレットを作成することで対応した。

    def save_animation(self, ext):
        if len(FrameListView.Model.Frames) < 2: return
        images = []
        for frame in FrameListView.Model.Frames:
            image = Image.new('P', (frame.Pixels.Width, frame.Pixels.Height))
            image.putpalette([0,0,0,255,255,255])
            image.putdata(frame.Pixels.Pixels.reshape(frame.Pixels.Width * frame.Pixels.Height).tolist())
            images.append(image)
        image.save(os.path.join(os.getcwd(), 'animation.' + ext), save_all=True, append_images=images, duration=100, loop=0, optimize=False)

バグ

  1. ドット描画時にドット抜けする
  2. ドット描画時にリストのアイコンが再描画されない

1. ドット描画時にドット抜けする

 マウスを高速にドラッグすると、ドット抜けしてしまう。

 ドット描画アルゴリズムを変更すれば解決できるかもしれない。つまりmouseMoveEvent発火時に現在座標を保存しておき、マウスボタンを離してドロップしたとき、座標間を線で結ぶようにする。面倒そうなので後回しにしている。

2. ドット描画時にリストのアイコンが再描画されない

項目
期待 ドット描画時にリストのアイコンを再描画したい
実際 リストをクリックすると再描画される

 頑張ったが、できなかった。

  • update(), repaint()しても再描画されなかった
  • なぜかmouseMoveEvent()を発火すると再描画された
    • リスト項目の先頭が選択されてしまう別のバグが生じるため不採用
FrameListView.mouseMoveEvent(QtGui.QMouseEvent(QtCore.QEvent.MouseMove, event.pos(), event.button(), event.buttons(), QtCore.Qt.NoModifier))

 対処不能のため、放置せざるを得ない。リストをクリックすれば再描画される。それで我慢する。

所感

 他にも以下をしたかった。が、別のUIが必要になるため、今回はやめた。

  • アニメ
    • 入力
      • 間隔
      • ループ回数
    • 表示
      • オニオン

 以下の機能くらいは欲しい。

  • キャンバスサイズの変更
  • パレット(最大16色)

 他にも山ほど要件がある。

要件

  • 色深度(1/2/4/8bit color)
  • アルファチャンネル付き
  • 指定インデックスを透明色にする
  • レイヤ
  • 各種描画ツール
  • 左右反転・上下反転
  • パレット編集(index入替)
  • 色見本、配色見本
  • パレット提案、配色パターン提案
  • キーコンフィグ
  • マウスジェスチャ
  • 手書き入力
  • コマンド入力操作
  • 音声入力
  • プログラム入力(マクロ入力)
  • プロセス間通信(自作ミニツールとの連動(アドオン))
  • アドオンSDK
  • マップエディタ
  • ゲームエンジン(物理演算、衝突判定、キー応答など)
  • 3D連携(ボクセル作成、3Dテクスチャ)
  • リプレイ動画用データ(テキストエディタで編集し解説文を簡単に表示できるとか)
  • サイトジェネレータ
  • 外部サービス連携
  • IPFSでサイト構築する

 雰囲気から色見本や配色パターンを提案して欲しい。

 マウスやショートカットキーだけでなく、vimのようにコマンド入力でも操作したい。たとえばdrawrect 0 0 10 10のように。

 リプレイ動画をテキストファイルで保存したい。それを読込すればそのまま再生される。これを見れば誰かがドット絵を作った過程をそのまま再現できる。ついでに解説文の表示などもできればなおよし。

 ドット絵ファーストでゲーム作りができたら楽しそう。そこまでできなくとも、ドット絵を動かせるエディタが欲しい。キー入力に応じてアニメーションするような。方向キーで歩行グラ、Aキーで攻撃アニメ、Bキーでジャンプとか。アニメと連動する効果音も作成できたらなおよし。

 3Dとも連携させたい。3Dドット絵(ボクセル)を作るとか、3Dポリゴンにテクスチャとしてドット絵を貼って表示するとか。それを動画にしてgif,apng,webp,mp4などに出力するとか。

 サイト作成も自動化したい。GitHubPages等のホストを使って。ドット絵やイラスト集、講座系、ミニゲーム。それらをドット絵を書いただけで作ってくれるような。

 TweetやToot,ピンタレストなど外部サービス連携も実装したい。サッと描いてすぐ投稿できるような。あるいは1日寝かせて予約投稿するとか。中央集権ネットワークから脱却し、IPFSを使ってサイト構築できるとなおよし。

 要件が多すぎて絶対ムリだろうな。

対象環境

$ uname -a
Linux raspberrypi 4.19.97-v7l+ #1294 SMP Thu Jan 30 13:21:14 GMT 2020 armv7l GNU/Linux