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.
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:
# 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.

*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).
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:

- 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.
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()