painter.setCompositionMode(QtGui.QPainter.CompositionMode_Source)
が必要だった。
成果物
format | image | animation |
---|---|---|
gif |
||
png |
||
webp |
||
txt |
pixels.txt | - |
コード
コード(688行)
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 mouseReleaseEvent(self, event): super(self.__class__, self).mouseReleaseEvent(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.animation = AnimationWidget() # self.animation.setMinimumHeight(self.animation.height()) # self.animation.setMaximumHeight(self.animation.height()*1.2) self.animation.setMinimumHeight(32) self.animation.setMaximumHeight(64) self.animation.resize(self.animation.width(), self.animation.height()) globals()['AnimationWidget'] = self.animation scroller1 = QtWidgets.QScrollArea() scroller1.setWidget(self.view) layout = QtWidgets.QGridLayout() layout.addWidget(scroller1, 0, 0) layout.addWidget(self.animation, 1, 0) 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 mouseReleaseEvent(self, event): super(self.__class__, self).mouseReleaseEvent(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() def mouseReleaseEvent(self, event): super(self.__class__, self).mouseReleaseEvent(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 mouseReleaseEvent(self, event): for item in self.items(): item.mouseReleaseEvent(event) super(self.__class__, self).mousePressEvent(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() img = QtGui.QImage(self.pixels.Width, self.pixels.Height, QtGui.QImage.Format_ARGB32) img.fill(QtGui.QColor(0,0,0,0)) # self.pixmap = QtGui.QPixmap(self.pixels.Width, self.pixels.Height) self.pixmap = QtGui.QPixmap.fromImage(img) print('Alpha:', self.pixmap.hasAlpha()) self.__draw_pos = [] self.freehand = FreeHand() 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)) painter.drawPixmap(0, 0, self.pixels.Width*self.scale, self.pixels.Height*self.scale, self.pixmap) def mouseMoveEvent(self, event): pos = event.scenePos() x = int(pos.x()//self.scale) y = int(pos.y()//self.scale) x = max(0, x) y = max(0, y) x = min(x, self.pixels.Width-1) y = min(y, self.pixels.Height-1) if event.buttons() & QtCore.Qt.LeftButton: self.freehand.Color = QtGui.QColor(255,0,0) self.freehand.draw(self.pixmap, x, y) self.pixels.Pixels[y][x] = 1 for idx in FrameListView.selectedIndexes(): FrameListModel.Frames[idx.row()].Pixels.Pixels[y][x] = 1 FrameListModel.update_pixmap(idx, self.pixmap) # 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.freehand.Color = QtCore.Qt.transparent # self.freehand.Color = QtCore.Qt.color0 self.freehand.Color = QtGui.QColor(0,0,0,0) self.freehand.draw(self.pixmap, x, y) self.pixels.Pixels[y][x] = 0 for idx in FrameListView.selectedIndexes(): FrameListModel.Frames[idx.row()].Pixels.Pixels[y][x] = 0 # FrameListModel.update_icon(idx) FrameListModel.update_pixmap(idx, self.pixmap) # 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) x = max(0, x) y = max(0, y) x = min(x, self.pixels.Width-1) y = min(y, self.pixels.Height-1) if event.buttons() & QtCore.Qt.LeftButton: self.freehand.Color = QtGui.QColor(255,0,0) self.freehand.draw(self.pixmap, x, y) # self.__draw_pos.append((x,y)) self.pixels.Pixels[y][x] = 1 for idx in FrameListView.selectedIndexes(): FrameListModel.Frames[idx.row()].Pixels.Pixels[y][x] = 1 # FrameListModel.update_icon(idx) FrameListModel.update_pixmap(idx, self.pixmap) # 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.freehand.Color = QtCore.Qt.transparent # self.freehand.Color = QtCore.Qt.color0 self.freehand.Color = QtGui.QColor(0,0,0,0) self.freehand.draw(self.pixmap, x, y) self.pixels.Pixels[y][x] = 0 for idx in FrameListView.selectedIndexes(): FrameListModel.Frames[idx.row()].Pixels.Pixels[y][x] = 0 # FrameListModel.update_icon(idx) FrameListModel.update_pixmap(idx, self.pixmap) # 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): pos = event.scenePos() x = int(pos.x()//self.scale) y = int(pos.y()//self.scale) x = max(0, x) y = max(0, y) x = min(x, self.pixels.Width-1) y = min(y, self.pixels.Height-1) self.freehand.draw(self.pixmap, x, y) self.freehand.points.clear() def mouseDoubleClickEvent(self, event): pass @property def Pixels(self): return self.pixels @Pixels.setter def Pixels(self, 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 FreeHand: def __init__(self, *args, **kwargs): self.points = [] self.color = QtGui.QColor(255,0,0,0) @property def Color(self): return self.color @Color.setter def Color(self, value): if isinstance(value, QtGui.QColor): self.color = value def draw(self, pixmap, x, y): self.points.append((x,y)) painter = QtGui.QPainter(pixmap) # painter.setBrush(QtGui.QBrush(QtGui.QColor(255,0,0,128), QtCore.Qt.SolidPattern)) painter.setBrush(self.color) painter.setPen(self.color) painter.setCompositionMode(QtGui.QPainter.CompositionMode_Source) # painter.fillRect(pixmap.rect(), QtCore.Qt.transparent); # painter.setCompositionMode(QtGui.QPainter.CompositionMode_SourceOver); if 1 == len(self.points): painter.drawLine(self.points[0][0], self.points[0][1], self.points[0][0], self.points[0][1]) elif 1 < len(self.points): for i in range(len(self.points)-1): painter.drawLine(self.points[i][0], self.points[i][1], self.points[i+1][0], self.points[i+1][1]) print(self.points[i][0], self.points[i][1], self.points[i+1][0], self.points[i+1][1]) painter.end() 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): for index in FrameListView.selectedIndexes(): qimg = FrameListView.Model.Frames[index.row()].pixmap.toImage() image = Image.new('1', (self.width, self.height)) image.putdata([0 if 0 == qimg.pixel(x,y) else 1 for y in range(qimg.width()) for x in range(qimg.height())]) print(ext) image.save(os.path.join(os.getcwd(), 'pixels.' + ext), optimize=True, lossless=True, disposal=2, transparency=0) 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]) qimg = frame.pixmap.toImage() image.putdata([0 if 0 == qimg.pixel(x,y) else 1 for y in range(frame.pixmap.width()) for x in range(frame.pixmap.height())]) images.append(image) p = {} p['save_all'] = True p['append_images'] = images p['duration'] = AnimationDurationSetDialog.Duration p['loop'] = AnimationDurationSetDialog.Loop p['optimize'] = False p['transparency'] = 0 if 'gif' == ext: p['disposal'] = 2 image.save(os.path.join(os.getcwd(), 'animation.' + ext), **p) 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 AnimationWidget(QtWidgets.QWidget): def __init__(self, parent=None): super(self.__class__, self).__init__(parent) self.frame_list = FrameListView() globals()['FrameListView'] = self.frame_list self.label = AnimationLabel() globals()['AnimationLabel'] = self.label layout = QtWidgets.QBoxLayout(QtWidgets.QBoxLayout.LeftToRight) layout.addWidget(self.label) layout.addWidget(self.frame_list) self.setLayout(layout) @property def Label(self): return self.label @property def FrameListView(self): return self.frame_list class AnimationLabel(QtWidgets.QLabel): def __init__(self, parent=None): super(self.__class__, self).__init__(parent) self.__is_stop = True self.__frame_index = 0 self.start_animation() def mousePressEvent(self, event): if event.buttons() & QtCore.Qt.LeftButton: self.__is_stop = not self.__is_stop print('is_stop:', self.__is_stop) self.start_animation() def start_animation(self): self.setPixmap(FrameListView.Model.Frames[self.__frame_index].Icon.pixmap(16,16)) if not self.__is_stop: if self.__frame_index < FrameListView.Model.rowCount()-1: self.__frame_index += 1 else: self.__frame_index = 0 QtCore.QTimer.singleShot(AnimationDurationSetDialog.Duration, self.start_animation) 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(16*32, 32) self.actions = {} self.__create_add_frame_action() self.__create_delete_frame_action() self.setCurrentIndex(self.model.index(0,0)) self.setFlow(QtWidgets.QListView.LeftToRight) self.duration_dialog = AnimationDurationSetDialog() globals()['AnimationDurationSetDialog'] = self.duration_dialog 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 Drawable.pixmap = frame.pixmap Window.widget.view.scene().update() if event.buttons() & QtCore.Qt.RightButton: self.duration_dialog.show() def update_pixmap(self, pixmap): for idx in self.selectedIndexes(): self.model.update_icon(idx, pixmap) @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() 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_pixmap(self, index, pixmap): self.frames[index.row()].update_pixmap(pixmap) @property def Frames(self): return self.frames class Frame: def __init__(self): self.pixels = Pixels() img = QtGui.QImage(self.pixels.Width, self.pixels.Height, QtGui.QImage.Format_ARGB32) img.fill(QtGui.QColor(0,0,0,0)) # self.pixmap = QtGui.QPixmap(self.pixels.Width, self.pixels.Height) # self.__init_pixmap() self.pixmap = QtGui.QPixmap.fromImage(img) self.icon = QtGui.QImage(self.pixels.Width, self.pixels.Height, QtGui.QImage.Format_Mono) self.update_pixmap(self.pixmap) def __init_pixmap(self): painter = QtGui.QPainter(self.pixmap) painter.setBrush(QtGui.QBrush(QtGui.QColor(0,0,0), QtCore.Qt.SolidPattern)) painter.fillRect(self.pixmap.rect(), QtGui.QColor(0,0,0)) painter.end() def update_pixmap(self, pixmap): self.pixmap = pixmap self.icon = QtGui.QIcon(pixmap) @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 class AnimationDurationSetDialog(QtWidgets.QDialog): def __init__(self, parent=None): super(self.__class__, self).__init__(parent) self.setWindowTitle("Set duration") self.duration = QtWidgets.QSpinBox() self.loop = QtWidgets.QSpinBox() self.duration.setMinimum(0) self.duration.setMaximum(1000*60*60*24) self.duration.setSingleStep(1) self.loop.setMinimum(0) self.loop.setMaximum(2**30) self.loop.setSingleStep(1) self.duration.setValue(100) self.loop.setValue(0) layout = QtWidgets.QFormLayout() layout.addRow("Duration", self.duration) layout.addRow("Loop time", self.loop) self.setLayout(layout) self.x = self.geometry().x() self.y = self.geometry().y() self.w = 0 self.h = 0 def show(self): self.setGeometry(self.x, self.y, self.w, self.h) super(self.__class__, self).show() def keyPressEvent(self, event): if event.key() == QtCore.Qt.Key_Escape: self.x = self.geometry().x() self.y = self.geometry().y() self.w = self.geometry().width() self.h = self.geometry().height() super(self.__class__, self).keyPressEvent(event) @property def Duration(self): return self.duration.value() @property def Loop(self): return self.loop.value() if __name__ == "__main__": app = QtWidgets.QApplication(sys.argv) window = Window() sys.exit(app.exec_())
python3 main.py
苦労した所
透明色を上書きする
最初は透明色QColor(0,0,0,0)
をそのまま描画しようとしたが、なぜかできなかった。以下の設定をしてから描画することで成功した。
painter.setCompositionMode(QtGui.QPainter.CompositionMode_Source)
どうやら以下のようなモードもあるらしい。
painter.setCompositionMode(QtGui.QPainter.CompositionMode_SourceOver)
察するに、これまではSourceOver
だったのだろう。名前からみても「上塗り」するモード。透明色を上塗りしても、赤色は消えない。なにせ透明色を重ねた所で透明だから下の色が透けるだけ。
Source
モードは直接代入なのだろう。透明色をそのままセットする。赤色が透明色に置き換わるため、ちゃんと消えてくれる。
ファイル出力
Pixmap化に伴い、ファイル出力もPixmapデータを出力するようにした。
じつは前回のはこれができていなかった。せっかく歯抜けドットを改善したのに、出力ファイルには反映されていなかった。
ついでにパラメータを見直してみた。
parameter | 意味 |
---|---|
save_all |
アニメ出力するときはTrue |
append_images |
アニメ出力するとき画像リストを渡す |
duration |
アニメ間隔(ミリ秒) |
loop |
ループ回数 |
optimize |
最適化。アニメ出力するときはFalse にしないと勝手にコマ抜きされうる |
transparency |
透明色とするパレットインデックス番号。0 が一般的。 |
disposal |
表示後の設定。gif 形式のとき0 〜3 のいずれかを指定する。PNGアニメ出力時にこれがあるとエラーになる。0 :未指定、1 :破棄しない、2 :背景色に戻す、3 :以前のコンテンツに復元する。 |
gif
アニメのときtransparency=0
を指定すると、前フレームが描画されてしまう。そこでdisposal=2
を指定してやる。ところがPNGアニメ出力時、エラーになる。そこでgif
形式のときだけdisposal=2
を追加してやることで正常に出力された。
バグ
- ドット描画時にリストのアイコンが再描画されない
- DnDによるファイル読込が機能しない(Pixmap化に伴う変更により)
所感
実際に触ってみると細かい罠が多数ある。思い通りのものを作るためにハンパない試行錯誤が必要。
1色アニメツールとしての機能は出揃ったか。あとは使いやすくするだけ。アイデアを洗い出してみる。沢山あった。思ったより大変そう……。その前にバグ修正か。
アイデア
- フレーム追加
Ctrl
+Alt
+N
したら現在フレームのコピーをその直後の位置へ新規追加し、その項目を選択するようにしたい- 現状、
Ctrl
+Alt
+N
後、いちいちリストで追加項目をクリックせねば選択されず面倒 - 既存フレームの絵をコピーしたフレームを新規追加したほうがいい場合もある
- なぜならアニメは前フレームとほんの少し違うだけで、ほぼ前フレームと同じ画像だから
- どのフレームをコピーする?
- 選択項目フレーム
- 挿入位置は末尾でいいか?
- 選択項目フレームの直後がいいのでは?
- 現状、
- duration設定ダイアログ
- 以下の方法でも表示できるようにしたい
- メニュー
- ショートカットキー:
Ctrl
+Shift
+Alt
+D
- ショートカットキー:
- アニメラベルを右クリックする
- メニュー
- 以下の方法でも表示できるようにしたい
以下の基本機能も欲しい。
- キャンバスサイズ変更
- 表示の拡大・縮小
キーボードだけで入力したい。以下のように。
キー | 意味 |
---|---|
↑ ,↓ ,← ,→ |
ピクセル位置移動 |
Space |
ドットを打つ/消す |
Alt +N |
アニメフレーム新規追加(現在フレームのコピー) |
Alt +Shift +N |
アニメフレーム新規追加(透明) |
Alt +D |
アニメフレーム削除 |
Alt +← ,→ |
選択フレーム変更 |
Alt +Shift +← ,→ |
選択フレーム移動 |
Alt +Enter |
アニメ間隔設定ダイアログ表示 |
Esc |
アニメ間隔設定ダイアログ非表示 |
Alt +Space |
アニメ再生/停止 |
+ ,- |
表示の拡大・縮小(ドット絵編集画面/アニメ再生画面) |
Ctrl +S |
ファイル出力 |
フレームごとのduration
設定ができたほうがいいかも?
- QListViewModel = QIcon + QSpinBox
- このときリストは縦表示にしたほうが見やすい
面倒ならduration
は全フレーム共通でいい。今まで通り。
ファイル出力設定できたほうがいいかも?
- 出力設定
- ファイルパス
- ディレクトリ
- ファイル名
- 拡張子
- ファイル形式
- 設定
- 共通
duration
,loop
,optimize
,transparency
gif
disposal
,palette
,interlace
,comment
png
compress_level
,bits
,dpi
,icc_profile
,exif
,srgb
,gamma
,...
- 共通
- ファイルパス
出力設定はなくてもいい。出力形式はファイル形式によって細かく設定できたほうが親切。でも面倒。現状のように自動で値を設定し、すべて出力させるほうが楽。
対象環境
- Raspbierry pi 4 Model B
- Raspbian buster 10.0 2019-09-26 ※
- bash 5.0.3(1)-release
- Qt 5.11
- Python 3.7.3
- PySide2
- Pillow 7.1.2
$ uname -a Linux raspberrypi 4.19.97-v7l+ #1294 SMP Thu Jan 30 13:21:14 GMT 2020 armv7l GNU/Linux