JustPaste.it

ReTextureMe Version 2

rtm.png

import tkinter as tk
from tkinter import filedialog, colorchooser
from PIL import Image, ImageTk

class PixelEditor:
    def __init__(self, root):
        self.root = root
        self.root.title("Pro Pixel Editor")
        
        # Configuration
        self.grid_size = 16
        self.scale = 25
        self.canvas_res = self.grid_size * self.scale
        
        self.image = Image.new("RGBA", (self.grid_size, self.grid_size), (0,0,0,0))
        self.history = []
        self.future = []
        self.max_history = 50 # Limit memory usage
        
        self.color = (0, 0, 0, 255)
        self.tool = "pen"
        self.dark = True
        self.show_grid = True
        self.last_pos = None

        # UI Setup
        self.setup_ui()
        
        # Canvas
        self.canvas = tk.Canvas(root, width=self.canvas_res, height=self.canvas_res, highlightthickness=0)
        self.canvas.pack(pady=10, padx=10)

        # Binds
        self.canvas.bind("<B1-Motion>", self.paint)
        self.canvas.bind("<Button-1>", self.start_paint)
        self.canvas.bind("<ButtonRelease-1>", self.reset_drag)
        self.canvas.bind("<Button-3>", self.pick_from_canvas)
        
        # Keyboard Shortcuts
        self.root.bind("<Control-z>", lambda e: self.undo())
        self.root.bind("<Control-y>", lambda e: self.redo())
        self.root.bind("<Control-s>", lambda e: self.save())

        self.apply_theme()
        self.draw()

    def setup_ui(self):
        self.toolbar = tk.Frame(self.root)
        self.toolbar.pack(fill="x", padx=5, pady=5)

        self.status = tk.Label(self.root, text="Tool: PEN", font=("Arial", 10, "bold"))
        self.status.pack(fill="x")

        # Create buttons with a loop for cleaner code
        btns = [
            ("Load", self.load),
            ("Save", self.save),
            ("Color", self.pick_color),
            ("Pen", lambda: self.set_tool("pen")),
            ("Eraser", lambda: self.set_tool("eraser")),
            ("Undo (Ctrl+Z)", self.undo),
            ("Redo (Ctrl+Y)", self.redo),
            ("Grid", self.toggle_grid),
            ("Theme", self.toggle_theme),
        ]

        self.btn_objs = {}
        for text, cmd in btns:
            b = tk.Button(self.toolbar, text=text, command=cmd, relief="flat", padx=10)
            b.pack(side="left", padx=2)
            self.btn_objs[text] = b

        self.update_tool_ui()

    def apply_theme(self):
        bg = "#1e1e1e" if self.dark else "#eeeeee"
        fg = "#ffffff" if self.dark else "#000000"
        btn_bg = "#333333" if self.dark else "#dddddd"

        self.root.configure(bg=bg)
        self.toolbar.configure(bg=bg)
        self.status.configure(bg=bg, fg=fg)

        for b in self.btn_objs.values():
            b.configure(bg=btn_bg, fg=fg, activebackground="#555555")

        self.canvas.configure(bg="#2b2b2b" if self.dark else "#ffffff")

    def toggle_theme(self):
        self.dark = not self.dark
        self.apply_theme()

    def toggle_grid(self):
        self.show_grid = not self.show_grid
        self.draw()

    def set_tool(self, tool):
        self.tool = tool
        self.update_tool_ui()

    def update_tool_ui(self):
        self.status.config(text=f"Tool: {self.tool.upper()}")
        # Highlight active tool
        for name, obj in self.btn_objs.items():
            if name.lower() == self.tool:
                obj.configure(relief="sunken", bg="#555555" if self.dark else "#bbb")
            elif name.lower() in ["pen", "eraser"]:
                obj.configure(relief="flat", bg="#333333" if self.dark else "#ddd")

    def pick_color(self):
        c = colorchooser.askcolor()[0]
        if c:
            self.color = (int(c[0]), int(c[1]), int(c[2]), 255)

    def pick_from_canvas(self, event):
        x, y = event.x // self.scale, event.y // self.scale
        if 0 <= x < self.grid_size and 0 <= y < self.grid_size:
            self.color = self.image.getpixel((x, y))

    # --- UNDO / REDO LOGIC UPGRADED ---
    
    def save_to_history(self):
        """Saves current state before a new stroke starts."""
        # Add current image to history
        self.history.append(self.image.copy())
        # Prevent history from getting too huge
        if len(self.history) > self.max_history:
            self.history.pop(0)
        # Clear redo stack whenever a new action is performed
        self.future.clear()

    def undo(self):
        if self.history:
            self.future.append(self.image.copy())
            self.image = self.history.pop()
            self.draw()

    def redo(self):
        if self.future:
            self.history.append(self.image.copy())
            self.image = self.future.pop()
            self.draw()

    # --- PAINTING LOGIC ---

    def start_paint(self, event):
        # Save state ONCE at the start of the click
        self.save_to_history()
        self.last_pos = (event.x // self.scale, event.y // self.scale)
        self.paint(event)

    def reset_drag(self, event):
        self.last_pos = None

    def paint(self, event):
        x = event.x // self.scale
        y = event.y // self.scale

        if not (0 <= x < self.grid_size and 0 <= y < self.grid_size):
            return

        if self.last_pos:
            # draw_line updates the self.image pixels
            self.draw_line(self.last_pos, (x, y))
        else:
            self.apply_pixel(x, y)

        self.last_pos = (x, y)
        self.draw()

    def draw_line(self, p1, p2):
        """Bresenham's Line Algorithm to ensure no gaps when mouse moves fast."""
        x1, y1 = p1
        x2, y2 = p2
        dx, dy = abs(x2 - x1), abs(y2 - y1)
        sx = 1 if x1 < x2 else -1
        sy = 1 if y1 < y2 else -1
        err = dx - dy

        while True:
            self.apply_pixel(x1, y1)
            if x1 == x2 and y1 == y2: break
            e2 = 2 * err
            if e2 > -dy: err -= dy; x1 += sx
            if e2 < dx: err += dx; y1 += sy

    def apply_pixel(self, x, y):
        color = self.color if self.tool == "pen" else (0,0,0,0)
        self.image.putpixel((x, y), color)

    def draw(self):
        """Refreshes the canvas display."""
        # Resize image for preview
        preview = self.image.resize((self.canvas_res, self.canvas_res), Image.NEAREST)
        self.tk_img = ImageTk.PhotoImage(preview)
        
        self.canvas.delete("all")
        self.canvas.create_image(0, 0, anchor="nw", image=self.tk_img)

        if self.show_grid:
            color = "#444444" if self.dark else "#cccccc"
            for i in range(self.grid_size + 1):
                p = i * self.scale
                self.canvas.create_line(p, 0, p, self.canvas_res, fill=color)
                self.canvas.create_line(0, p, self.canvas_res, p, fill=color)

    def load(self):
        path = filedialog.askopenfilename(filetypes=[("PNG","*.png")])
        if path:
            self.save_to_history()
            self.image = Image.open(path).convert("RGBA").resize((self.grid_size, self.grid_size), Image.NEAREST)
            self.draw()

    def save(self):
        path = filedialog.asksaveasfilename(defaultextension=".png")
        if path:
            self.image.save(path, "PNG")

if __name__ == "__main__":
    root = tk.Tk()
    app = PixelEditor(root)
    root.mainloop()