Skip to content

Demo Exporter

Aby wyeksportować tę animację z Pythona do plików MP4 i GIF, nie da się użyć prostego przycisku „Zapisz jako”, ponieważ biblioteka tkinter służy wyłącznie do wyświetlania okien na żywo. Możesz to jednak zrobić bardzo profesjonalnie na dwa sposoby: automatycznym kodem (idealne do seryjnej produkcji materiałów) lub zewnętrznym programem (najszybsze jednorazowo).


Sposób 1: Automatyczny eksport za pomocą Pythona (Zalecany)

Możesz zmodyfikować kod tak, aby zamiast wyświetlać okno na ekranie, program zapisywał każdą klatkę jako obrazek PNG, a następnie automatycznie połączył je w plik MP4 i GIF.
Do tego sposobu potrzebujesz zainstalować dwie popularne biblioteki (wytnij na chwilę pętlę prezentacji i wpisz w terminalu):

pip install pillow opencv-python

Zapisz poniższy skrypt jako enigma_exporter.py i uruchom go. Program wygeneruje dokładnie 50 klatek animacji, zamknie się i stworzy pliki enigma_animation.mp4 oraz enigma_animation.gif w tym samym folderze.

Python
import tkinter as tk
import random
import string
import os
from PIL import Image, ImageGrab
import cv2

class EnigmaExporter:
    def __init__(self, root):
        self.root = root
        self.root.title("Enigma Exporter")
        self.root.geometry("1000x750")
        self.root.configure(bg="#121212")

        self.ROTOR_WIRING = {'I': "EKMFLGDQVZNTOWYHXUSPAIBRCJ", 'II': "AJDKSIRUXBLHWTMCQGZNPYFVOE", 'III': "BDFHJLCPRTXVZNYEIWGAKMUSQO"}
        self.NOTCHES = {'I': 'Q', 'II': 'E', 'III': 'V'}
        self.REFLECTOR = "YRUHQSLDPXNGOKMIEBFZCWVJAT"

        self.r_pos = [0, 0, 0]
        self.rotors = ['I', 'II', 'III']
        self.active_input_key = None
        self.active_output_lamp = None

        # Liczniki klatek do zapisu wideo
        self.frame_count = 0
        self.max_frames = 50  # Ile kroków maszyny chcesz nagrać (ok. 15-20 sekund animacji)
        self.frame_files = []

        self._setup_ui()
        # Dajemy systemowi 1 sekundę na poprawne wyrenderowanie okna przed zrzutem ekranu
        self.root.after(1000, self._animate_and_capture)

    def _setup_ui(self):
        header = tk.Label(self.root, text="AUTOMATIC ENIGMA SIMULATION", font=("Courier", 24, "bold"), fg="#00FF66", bg="#121212")
        header.pack(pady=15)

        self.rotor_frame = tk.Frame(self.root, bg="#121212")
        self.rotor_frame.pack(pady=10)

        self.rotor_widgets = []
        for i, name in enumerate(["LEWY (I)", "ŚRODKOWY (II)", "PRAWY (III)"]):
            f = tk.LabelFrame(self.rotor_frame, text=name, font=("Courier", 10, "bold"), fg="#888888", bg="#1e1e1e", padx=15, pady=5, bd=2)
            f.grid(row=0, column=i, padx=25)
            drum_box = tk.Frame(f, bg="#2a2a2a", bd=2, relief="sunken")
            drum_box.pack()

            lbl_top = tk.Label(drum_box, text="Z", font=("Courier", 20), fg="#555555", bg="#2a2a2a", width=3)
            lbl_top.pack()
            lbl_mid = tk.Label(drum_box, text="A", font=("Courier", 44, "bold"), fg="#ffffff", bg="#3a3a3a", width=3)
            lbl_mid.pack(pady=2)
            lbl_bot = tk.Label(drum_box, text="B", font=("Courier", 20), fg="#555555", bg="#2a2a2a", width=3)
            lbl_bot.pack()
            self.rotor_widgets.append((lbl_top, lbl_mid, lbl_bot))

        self.canvas = tk.Canvas(self.root, width=900, height=140, bg="#1a1a1a", highlightthickness=1, highlightbackground="#333333")
        self.canvas.pack(pady=15)

        self.board_frame = tk.Frame(self.root, bg="#121212")
        self.board_frame.pack(pady=10)

        self.keys = {}
        rows = ["QWERTZUIO", "ASDFGHJK", "PYXCVBNML"]
        for row in rows:
            fr = tk.Frame(self.board_frame, bg="#121212")
            fr.pack(pady=3)
            for char in row:
                l = tk.Label(fr, text=char, font=("Courier", 16, "bold"), fg="#888888", bg="#222222", width=4, height=2, bd=2, relief="raised")
                l.pack(side=tk.LEFT, padx=4)
                self.keys[char] = l

    def _update_rotor_displays(self):
        for i in range(3):
            pos = self.r_pos[i]
            self.rotor_widgets[i][0].config(text=chr(((pos - 1) % 26) + 65))
            self.rotor_widgets[i][1].config(text=chr(pos + 65))
            self.rotor_widgets[i][2].config(text=chr(((pos + 1) % 26) + 65))

    def _step_rotors(self):
        l_notch, m_notch, p_notch = self.NOTCHES['I'], self.NOTCHES['II'], self.NOTCHES['III']
        l_char, m_char, p_char = chr(self.r_pos[0]+65), chr(self.r_pos[1]+65), chr(self.r_pos[2]+65)
        rot_l, rot_m, rot_p = False, False, True
        if m_char == m_notch:
            rot_l, rot_m = True, True
        elif p_char == p_notch:
            rot_m = True
        if rot_l: self.r_pos[0] = (self.r_pos[0] + 1) % 26
        if rot_m: self.r_pos[1] = (self.r_pos[1] + 1) % 26
        if rot_p: self.r_pos[2] = (self.r_pos[2] + 1) % 26
        self._update_rotor_displays()

    def _encrypt(self, char):
        self._step_rotors()
        idx = ord(char) - ord('A')
        for i in reversed(range(3)):
            shift = self.r_pos[i]
            idx = (ord(self.ROTOR_WIRING[self.rotors[i]][(idx + shift) % 26]) - ord('A') - shift) % 26
        idx = (ord(self.REFLECTOR[idx]) - ord('A')) % 26
        for i in range(3):
            shift = self.r_pos[i]
            target_char = chr((idx + shift) % 26 + 65)
            idx = (self.ROTOR_WIRING[self.rotors[i]].index(target_char) - shift) % 26
        return chr(idx + 65)

    def _redraw_canvas(self, in_char, out_char):
        self.canvas.delete("all")
        stages = [("WEJŚCIE", 70), ("R_PRAWY", 250), ("R_ŚROD", 430), ("R_LEWY", 610), ("REFLEKTOR", 810)]
        for name, x in stages:
            fill_color = "#333333"
            text_color = "#ffffff"
            if name == "WEJŚCIE": 
                fill_color = "#4c3c00"
                text_color = "#FFCC00"
            elif name == "REFLEKTOR":
                fill_color = "#1f3a23"
            self.canvas.create_rectangle(x-55, 30, x+55, 90, fill=fill_color, outline="#555555", width=2)
            self.canvas.create_text(x, 48, text=name, fill=text_color, font=("Courier", 11, "bold"))

        self.canvas.create_text(70, 75, text=f"[{in_char}]", fill="#FFCC00", font=("Courier", 16, "bold"))
        self.canvas.create_text(810, 75, text=f"({out_char})", fill="#00FF66", font=("Courier", 16, "bold"))
        self.canvas.create_line(125, 50, 755, 50, fill="#FFCC00", width=3, arrow=tk.LAST)
        self.canvas.create_line(755, 70, 125, 70, fill="#00FF66", width=3, arrow=tk.LAST)
        self.canvas.create_text(450, 120, text=f"STRUMIEŃ: Generowanie impulsu dla '{in_char}' -> Otrzymano kod '{out_char}'", fill="#888888", font=("Courier", 11))

    def _animate_and_capture(self):
        if self.frame_count >= self.max_frames:
            self._compile_files()
            return

        if self.active_input_key: self.keys[self.active_input_key].config(fg="#888888", bg="#222222")
        if self.active_output_lamp: self.keys[self.active_output_lamp].config(fg="#888888", bg="#222222")

        input_char = random.choice(string.ascii_uppercase)
        output_char = self._encrypt(input_char)

        self.keys[input_char].config(fg="#121212", bg="#FFCC00")
        self.active_input_key = input_char
        self.keys[output_char].config(fg="#121212", bg="#00FF66")
        self.active_output_lamp = output_char

        self._redraw_canvas(input_char, output_char)

        # WAŻNE: Aktualizacja geometrii i zrzut klatki
        self.root.update()

        # Pobieranie współrzędnych okna systemowego
        x = self.root.winfo_rootx()
        y = self.root.winfo_rooty()
        w = self.root.winfo_width()
        h = self.root.winfo_height()

        # Wykonanie zrzutu fragmentu ekranu i zapis tymczasowy
        filename = f"frame_{self.frame_count:03d}.png"
        img = ImageGrab.grab(bbox=(x, y, x+w, y+h))
        img.save(filename)
        self.frame_files.append(filename)

        self.frame_count += 1
        # Szybkie renderowanie klatek bez opóźnień wyświetlania
        self.root.after(300, self._animate_and_capture)

    def _compile_files(self):
        """Metoda łącząca wyrenderowane PNG w końcowe pliki MP4 i GIF"""
        print("Kompilowanie wideo MP4...")
        first_img = cv2.imread(self.frame_files[0])
        height, width, layers = first_img.shape

        # 1. GENEROWANIE MP4 (2 klatki na sekundę, aby zachować czytelność kroków)
        fourcc = cv2.VideoWriter_fourcc(*'mp4v')
        video = cv2.VideoWriter('enigma_animation.mp4', fourcc, 2, (width, height))

        for file in self.frame_files:
            video.write(cv2.imread(file))
        video.release()

        # 2. GENEROWANIE GIF
        print("Kompilowanie animacji GIF...")
        images = [Image.open(f) for f in self.frame_files]
        images[0].save(
            'enigma_animation.gif',
            save_all=True,
            append_images=images[1:],
            duration=500, # Czas trwania klatki w ms
            loop=0        # Nieskończona pętla
        )

        # Czyszczenie plików tymczasowych PNG
        for file in self.frame_files:
            os.remove(file)

        print("Sukces! Wygenerowano pliki: enigma_animation.mp4 i enigma_animation.gif")
        self.root.destroy()

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

Sposób 2: Szybkie nagrywanie darmowym programem (Bez pisania kodu)

Jeśli zależy Ci na czasie i chcesz nagrać dokładnie to, co widzisz na ekranie bez instalowania dodatkowych paczek w Pythonie, skorzystaj z gotowych, darmowych narzędzi dla systemów Windows/Mac:

1. ScreenToGif (Najlepsze dedykowane narzędzie):
* Mały, darmowy program o otwartym kodzie źródłowym dla Windows.
* Po włączeniu nakładasz przezroczystą ramkę programu na okno uruchomionego wcześniej symulatora Enigmy w Pythonie.
* Klikasz Zapisz (Record), czekasz kilkanaście sekund, a potem program pozwala zapisać nagranie bezpośrednio jako zoptymalizowany plik .gif lub plik wideo .mp4.
2. Skróty systemowe (Tylko MP4):
* Windows: Kliknij Windows + Alt + R, aby uruchomić systemowy dyktafon gier (Xbox Game Bar), który nagra aktywne okno Pythona bezpośrednio do pliku MP4.
* Mac: Kliknij Cmd + Shift + 5, zaznacz okno programu i wybierz opcję "Nagraj wybraną część ekranu".


Jak osadzić wynik w materiałach Markdown?

Gdy uzyskasz plik .gif, prowadzący wykład może go błyskawicznie osadzić bezpośrednio w pliku tekstowym scenariusza szkoleniowego (Markdown). Współczesne edytory, takie jak Obsidian, VS Code, GitHub czy Notion, wyrenderują go jako płynny, ruchomy obrazek:

Markdown
# Scenariusz wykładu: Maszyny Turinga i Architektura von Neumanna  

## Część 2: Kryptografia a wczesne systemy obliczeniowe  
Podczas omawiania matematycznych podstaw łamania szyfrów, warto pokazać słuchaczom dynamiczną naturę maszyn wirnikowych.   
![Automatyczny schemat pracy Enigmy](enigma_animation.gif)  

*Wskazówka dla prowadzącego:* Zwróć uwagę słuchaczy na asynchroniczny obrót trzech bębnów (licznik po lewej) w momencie, gdy sygnał prądu przemierza okablowanie.  

Aby przeskalować wynikową animację na mniejszą, najwygodniej jest dokonać modyfikacji bezpośrednio w kodzie sekcji _compile_files() (w pliku enigma_exporter.py).
Dzięki temu program automatycznie zmniejszy zarówno plik wideo MP4, jak i obrazek GIF, oszczędzając czas i zachowując doskonałą ostrość czcionek.

Rozwiązanie 1: Automatyczne skalowanie w kodzie (Zalecane)

Wystarczy, że w swoim skrypcie enigma_exporter.py podmienisz dotychczasową metodę _compile_files na poniższą wersję. Dodałem do niej parametr scale_factor = 0.5, który zmniejsza wymiary obrazu dokładnie o połowę (z 1000x750 do 500x375 pikseli).

Python
    def _compile_files(self):  
        """Metoda łącząca i automatycznie zmniejszająca pliki MP4 i GIF"""  
        scale_factor = 0.5  # 0.5 to pomniejszenie o 50%. Zmień np. na 0.7 dla 70% wielkości.  

        print("Kompilowanie i skalowanie wideo MP4...")  
        first_img = cv2.imread(self.frame_files[0])  
        height, width, layers = first_img.shape  

        # Nowe docelowe wymiary obrazka  
        new_width = int(width * scale_factor)  
        new_height = int(height * scale_factor)  
        new_size = (new_width, new_height)  

        # 1. GENEROWANIE I SKALOWANIE MP4  
        fourcc = cv2.VideoWriter_fourcc(*'mp4v')  
        video = cv2.VideoWriter('enigma_animation_small.mp4', fourcc, 2, new_size)  

        for file in self.frame_files:  
            img = cv2.imread(file)  
            # Zmniejszanie klatki za pomocą algorytmu INTER_AREA (najlepszy do zmniejszania tekstu)  
            resized_img = cv2.resize(img, new_size, interpolation=cv2.INTER_AREA)  
            video.write(resized_img)  
        video.release()  

        # 2. GENEROWANIE I SKALOWANIE GIF  
        print("Kompilowanie i skalowanie animacji GIF...")  
        small_images = []  
        for file in self.frame_files:  
            img = Image.open(file)  
            # Wykorzystujemy filtr Resampling.LANCZOS, aby tekst w małym GIFie pozostał idealnie czytelny  
            resized_img = img.resize(new_size, Image.Resampling.LANCZOS)  
            small_images.append(resized_img)  

        small_images[0].save(  
            'enigma_animation_small.gif',  
            save_all=True,  
            append_images=small_images[1:],  
            duration=500,  
            loop=0  
        )  

        # Czyszczenie tymczasowych klatek PNG  
        for file in self.frame_files:  
            import os  
            os.remove(file)  

        print(f"Sukces! Wygenerowano mniejsze pliki ({new_width}x{new_height}):")  
        print(" -> enigma_animation_small.mp4\n -> enigma_animation_small.gif")  
        self.root.destroy()  

Rozwiązanie 2: Szybkie przeskalowanie w samym pliku Markdown

Jeśli wygenerowałeś już duży plik GIF i nie chcesz ponownie uruchamiać skryptu, możesz wymusić jego mniejszy rozmiar bezpośrednio w kodzie dokumentu Markdown (o ile edytor, z którego korzystasz, np. Obsidian lub GitHub, to wspiera).

  • Sposób dla Obsidian / Typora:
    Wystarczy dopisać pionową kreskę i szerokość w pikselach na końcu linku:

![Enigma](enigma_animation.gif|450)

  • Uniwersalny sposób (HTML) działający wszędzie (GitHub, Notion, VS Code):
    Zamiast standardowego zapisu Markdown, użyj znacznika graficznego HTML, w którym możesz precyzyjnie podać szerokość procentową lub pikselową:

<img src="enigma_animation.gif" width="500" alt="Symulacja Enigmy">

Aby wyeksportować wersję konsolową (tekstową) do formatu GIF, nie możemy użyć biblioteki PIL/ImageGrab, ponieważ systemowe okno konsoli (terminala) nie podlega pod kontrolę graficzną Pythona w taki sam sposób jak okno tkinter.
Najbardziej eleganckim i w 100% cyfrowym rozwiązaniem jest zmodyfikowanie kodu konsolowego tak, aby renderował tekst bezpośrednio na wirtualnym obrazku w pamięci, używając czcionki o stałej szerokości znaków (Monospace). Dzięki temu uzyskasz idealnie ostry, czysty plik GIF bez konieczności nagrywania ekranu.

Kod eksportera wersji konsolowej do GIF

Poniższy skrypt wykonuje dokładnie 10 pierwszych kroków symulacji tekstowej i generuje plik enigma_console.gif.
Kod automatycznie rysuje czarną powłokę terminala i nakłada zielone znaki. Wymaga zainstalowania biblioteki Pillow (którą już masz z poprzedniego kroku): pip install pillow.

Python
import random
from PIL import Image, ImageDraw, ImageFont

class EnigmaMachineConsole:
    def __init__(self):
        self.ROTOR_WIRING = {
            'I':   "EKMFLGDQVZNTOWYHXUSPAIBRCJ",
            'II':  "AJDKSIRUXBLHWTMCQGZNPYFVOE",
            'III': "BDFHJLCPRTXVZNYEIWGAKMUSQO"
        }
        self.ROTOR_NOTCH = {'I': 'Q', 'II': 'E', 'III': 'V'}
        self.REFLECTOR = "YRUHQSLDPXNGOKMIEBFZCWVJAT"
        self.r_pos = [0, 0, 0]
        self.rotors = ['I', 'II', 'III']

    def _rotate(self):
        m_char = chr(self.r_pos[1] + 65)
        p_char = chr(self.r_pos[2] + 65)
        rotate_left = False
        rotate_mid = False
        rotate_right = True

        if m_char == self.ROTOR_NOTCH[self.rotors[1]]:
            rotate_left = True
            rotate_mid = True
        elif p_char == self.ROTOR_NOTCH[self.rotors[2]]:
            rotate_mid = True

        if rotate_left:  self.r_pos[0] = (self.r_pos[0] + 1) % 26
        if rotate_mid:   self.r_pos[1] = (self.r_pos[1] + 1) % 26
        if rotate_right: self.r_pos[2] = (self.r_pos[2] + 1) % 26

    def _map_forward(self, char_idx, rotor_name, pos):
        shift_idx = (char_idx + pos) % 26
        out_char = self.ROTOR_WIRING[rotor_name][shift_idx]
        return (ord(out_char) - ord('A') - pos) % 26

    def _map_backward(self, char_idx, rotor_name, pos):
        shift_idx = (char_idx + pos) % 26
        out_char = chr(self.ROTOR_WIRING[rotor_name].index(chr(shift_idx + 65)) + 65)
        return (ord(out_char) - ord('A') - pos) % 26

    def encrypt_char(self, char):
        self._rotate()
        idx = ord(char) - ord('A')
        idx = self._map_forward(idx, self.rotors[2], self.r_pos[2])
        idx = self._map_forward(idx, self.rotors[1], self.r_pos[1])
        idx = self._map_forward(idx, self.rotors[0], self.r_pos[0])
        idx = (ord(self.REFLECTOR[idx]) - ord('A')) % 26
        idx = self._map_backward(idx, self.rotors[0], self.r_pos[0])
        idx = self._map_backward(idx, self.rotors[1], self.r_pos[1])
        idx = self._map_backward(idx, self.rotors[2], self.r_pos[2])
        return chr(idx + 65)

def generate_exact_scroll_gif():
    enigma = EnigmaMachineConsole()
    frames = []

    width, height = 600, 400
    bg_color = "#000000"       # Czysta czerń
    text_color = "#00FF66"     # Zielony terminal

    # Inteligentny wybór czcionki Monospace na Linux / Kubuntu
    try:
        # Próba załadowania domyślnego fontu konsolowego w Ubuntu/Kubuntu
        font = ImageFont.truetype("NimbusMonoPS-Regular.otf", 14)
    except IOError:
        try:
            # Rezerwowa prośba do systemu o jakikolwiek zainstalowany font stałoszerokościowy
            font = ImageFont.truetype("monospace", 14)
        except IOError:
            # Ostateczność (wbudowany font Pillow)
            font = ImageFont.load_default()

    raw_steps = []

    print("Generowanie klatek na Kubuntu...")
    for step in range(10):
        img = Image.new("RGB", (width, height), color=bg_color)
        draw = ImageDraw.Draw(img)

        # Nagłówek
        draw.text((20, 20), "=============================================================", fill=text_color, font=font)
        draw.text((20, 38), "  SYMULACJA HISTORII ENIGMY I — TRANSMISJA AUTOMATYCZNA", fill=text_color, font=font)
        draw.text((20, 56), "=============================================================", fill=text_color, font=font)

        char = random.choice("ABCDEFGHIJKLMNOPQRSTUVWXYZ")
        cipher_char = enigma.encrypt_char(char)
        pos_str = f"[{chr(enigma.r_pos[0]+65)}][{chr(enigma.r_pos[1]+65)}][{chr(enigma.r_pos[2]+65)}]"

        raw_steps.append((pos_str, char, cipher_char))

        # Rysowanie linii ciasno obok siebie (efekt braku pustych linii)
        start_y = 85
        line_spacing = 18 # Zmniejszony dystans - tekst idzie linia pod linią

        for idx, (p_str, in_c, out_c) in enumerate(raw_steps):
            current_y = start_y + (idx * line_spacing)

            if idx == len(raw_steps) - 1:
                line_text = f"BĘBNY: {p_str}  |  WEJŚCIE: {in_c}  --->  SZYFR: {out_c}"
            else:
                line_text = f"BĘBNY: {p_str}  |  WEJŚCIE: {in_c}  --->  SZYFR: {out_c}  [OK]"

            draw.text((20, current_y), line_text, fill=text_color, font=font)

        frames.append(img)

    print("Kompilowanie pliku enigma_terminal_exact.gif...")
    frames[0].save(
        "enigma_terminal_exact.gif",
        save_all=True,
        append_images=frames[1:],
        duration=1000,
        loop=0
    )
    print("Sukces! Wygenerowano poprawny plik na Twoim systemie.")

if __name__ == "__main__":
    generate_exact_scroll_gif()