From 4897a0afe11110c497547e7597f9ffa159ead1af Mon Sep 17 00:00:00 2001 From: AleczO Date: Sat, 7 Mar 2026 13:53:13 +0100 Subject: [PATCH] frontend wczesna wersja MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Piszcie dc jak coś nie będzie działać albo macie pytania --- experiment/Speller/__init__.py | 2 + experiment/Speller/grid.py | 65 +++++++++++++++++++++++++ experiment/Speller/menu.py | 24 ++++++++++ experiment/Speller/stimulus.py | 88 ++++++++++++++++++++++++++++++++++ experiment/config.py | 57 ++++++++++++++++++++++ experiment/gui/__init__.py | 0 experiment/main.py | 68 ++++++++++++++++++++++++++ 7 files changed, 304 insertions(+) create mode 100644 experiment/Speller/__init__.py create mode 100644 experiment/Speller/grid.py create mode 100644 experiment/Speller/menu.py create mode 100644 experiment/Speller/stimulus.py create mode 100644 experiment/config.py delete mode 100644 experiment/gui/__init__.py diff --git a/experiment/Speller/__init__.py b/experiment/Speller/__init__.py new file mode 100644 index 0000000..8835d20 --- /dev/null +++ b/experiment/Speller/__init__.py @@ -0,0 +1,2 @@ +from .stimulus import SSVEPStimulus +from .grid import SpellerGrid \ No newline at end of file diff --git a/experiment/Speller/grid.py b/experiment/Speller/grid.py new file mode 100644 index 0000000..ceeeaea --- /dev/null +++ b/experiment/Speller/grid.py @@ -0,0 +1,65 @@ +import pygame +from .stimulus import SSVEPStimulus + +class SpellerGrid: + def __init__(self, screen_res, frequencies, labels): + self.width, self.height = screen_res + self.stimuli = [] + + # 1. ------------------------------------ Struktura Obiektu ------------------------------------ + + # (3 kolumny, 2 rzędy = 6 kafelków) + cols = 3 + rows = 2 + + # Odstęp między kafelkami + margin = 80 + + # 2. ------------------------------------ Symetryczne ustawianie siatki ------------------------------------ + + + # 2. Obliczamy maksymalny możliwy rozmiar kafelka, który wejdzie na ekran + available_w = self.width - (cols + 1) * margin + available_h = self.height - (rows + 1) * margin + + tile_w = available_w // cols + tile_h = available_h // rows + + # Rozmiar boku kwadratu (wybieramy mniejszy, by zachować proporcje 1:1) + size = min(tile_w, tile_h) + + # Całkowita szerokość i wysokość całego boxa gridu + total_grid_width = (cols * size) + ((cols - 1) * margin) + total_grid_height = (rows * size) + ((rows - 1) * margin) + + + start_x = (self.width - total_grid_width) // 2 + start_y = (self.height - total_grid_height) // 2 + + # 5. ------------------------------------ Konstrukcja kafelków z uwzględnieniem offsetu ------------------------------------ + for i in range(len(labels)): + col = i % cols + row = i // cols + + # Nowa pozycja z uwzględnieniem start_x i start_y + x = start_x + col * (size + margin) + y = start_y + row * (size + margin) + + s = SSVEPStimulus(x, y, size, frequencies[i], label=labels[i]) + self.stimuli.append(s) + + + def update(self): + for s in self.stimuli: + s.update() + + + def draw(self, surface): + for s in self.stimuli: + s.draw(surface) + + + def set_labels(self, new_labels): + for i, label in enumerate(new_labels): + if i < len(self.stimuli): + self.stimuli[i].label = label \ No newline at end of file diff --git a/experiment/Speller/menu.py b/experiment/Speller/menu.py new file mode 100644 index 0000000..5e0ea0c --- /dev/null +++ b/experiment/Speller/menu.py @@ -0,0 +1,24 @@ +import pygame +import config +from Speller import SSVEPStimulus + +def draw_menu(screen): + screen.fill((0, 0, 0)) + + btn_offline = SSVEPStimulus(x=config.WIDTH//2 - 350, y=config.HEIGHT//2 - 100, + size=300, freq=0, label="OFFLINE (Calibration)") + + btn_online = SSVEPStimulus(x=config.WIDTH//2 + 50, y=config.HEIGHT//2 - 100, + size=300, freq=0, label="ONLINE\n(Speller)") + + btn_offline.current_color = (0, 130, 0) + btn_online.current_color = (130, 0, 0) + + btn_offline.draw(screen) + btn_online.draw(screen) + + font = pygame.font.SysFont('Arial', 24) + text = font.render("Press 1 for Offline or 2 for Online", True, config.COLOR_WHITE) + screen.blit(text, (config.WIDTH//2 - text.get_width()//2, config.HEIGHT//2 + 250)) + + return btn_offline.rect, btn_online.rect \ No newline at end of file diff --git a/experiment/Speller/stimulus.py b/experiment/Speller/stimulus.py new file mode 100644 index 0000000..c56b49a --- /dev/null +++ b/experiment/Speller/stimulus.py @@ -0,0 +1,88 @@ +import math +import pygame + +class SSVEPStimulus: + def __init__(self, x, y, size, freq, refresh_rate=60, label=""): + self.rect = pygame.Rect(x, y, size, size) + self.freq = freq + self.refresh_rate = refresh_rate + self.label = label + self.frame_count = 0 + + # Kolory + self.current_color = (0, 0, 0) + self.text_color = (255, 255, 255) + + pygame.font.init() + self.font = pygame.font.SysFont('Arial', int(40), bold=True) + + def update(self): + t = self.frame_count / self.refresh_rate + + sine_val = math.sin(2 * math.pi * self.freq * t) + + intensity = int(127.5 * (sine_val * 0.5 + 1.0)) + + self.current_color = (intensity, 0, 0) + + self.frame_count += 1 + + def draw(self, surface): + # Renderowanie boxa + pygame.draw.rect(surface, self.current_color, self.rect) + + # Renderowanie liter w boxie + if self.label: + # Margines wewnętrzny + padding_percent = 0.05 + target_width = int(self.rect.width * (1 - padding_percent * 2)) + + # --- Algorytm zawijania tekstu --- + + explicit_segments = self.label.split('\n') + final_lines = [] + + line_height = self.font.get_linesize() + + # Przetwarzamy każdy segment + for segment in explicit_segments: + + words = segment.split(' ') + if not words or (len(words) == 1 and words[0] == ''): + final_lines.append("") + continue + + current_line_words = [] + + for word in words: + test_line_str = ' '.join(current_line_words + [word]) + + width, height = self.font.size(test_line_str) + + if width <= target_width: + + current_line_words.append(word) + else: + + final_lines.append(' '.join(current_line_words)) + current_line_words = [word] + + final_lines.append(' '.join(current_line_words)) + + # --- Renderowanie i wyśrodkowanie --- + + total_text_height = len(final_lines) * line_height + + start_y = self.rect.centery - (total_text_height // 2) + + # Renderujemy każdą linię po kolei + for i, line_str in enumerate(final_lines): + if not line_str: continue + + line_surf = self.font.render(line_str, True, self.text_color) + + line_rect = line_surf.get_rect(centerx=self.rect.centerx) + + line_rect.y = start_y + i * line_height + + surface.blit(line_surf, line_rect) \ No newline at end of file diff --git a/experiment/config.py b/experiment/config.py new file mode 100644 index 0000000..07da07c --- /dev/null +++ b/experiment/config.py @@ -0,0 +1,57 @@ + +# --- Ustawienia ekranu --- +WIDTH, HEIGHT = 1280, 720 +REFRESH_RATE = 60 + +# --- Parametry SSVEP --- +# 6 częstotliwości +FREQS = [7.5, 8.57, 10.0, 12.0, 15.0, 8.0] + +# --- Struktura Alfabetu (Drzewo) --- + +ALPHABET_TREE = { + "root": [ + "UNDO", + "START", + "A B C D\nE F G H\nI J K L\nM N O P", + "Q R S T\nU V W X\nY Z + -\n* / ( )", + "1 2 3 4\n5 6 7 8\n9 0", + "$ & @ \"\n. , ? !\n% : ; =\n~ # Del Blank" + ], + + # Poddrzewo Grupy 1 (Litery A-P) + "A B C D\nE F G H\nI J K L\nM N O P": [ + "BACK", "MAIN", + "A B C D", "E F G H", + "I J K L", "M N O P" + ], + + # Poddrzewo Grupy 2 (Litery Q-Z i symbole) + "Q R S T\nU V W X\nY Z + -\n* / ( )": [ + "BACK", "MAIN", + "Q R S T", "U V W X", + "Y Z + -", "* / ( )" + ], + + # Poddrzewo Grupy 3 (Cyfry) + "1 2 3 4\n5 6 7 8\n9 0": [ + "BACK", "MAIN", + "1 2", "3 4", + "5 6", "7 8 9 0" + ], + + # Liście (konkretne litery) - przykład dla pierwszej grupy + "A B C D": ["BACK", "MAIN", "A", "B", "C", "D"], + "E F G H": ["BACK", "MAIN", "E", "F", "G", "H"], + "I J K L": ["BACK", "MAIN", "I", "J", "K", "L"], + "M N O P": ["BACK", "MAIN", "M", "N", "O", "P"], + + # Przykład dla cyfr + "1 2": ["BACK", "MAIN", "1", "2", "-", "-"], +} + +TILE_MARGIN = 40 + +MENU_OPTIONS = ["1. OFFLINE", "2. ONLINE"] +COLOR_WHITE = (255, 255, 255) +COLOR_GRAY = (50, 50, 50) \ No newline at end of file diff --git a/experiment/gui/__init__.py b/experiment/gui/__init__.py deleted file mode 100644 index e69de29..0000000 diff --git a/experiment/main.py b/experiment/main.py index e69de29..506a02e 100644 --- a/experiment/main.py +++ b/experiment/main.py @@ -0,0 +1,68 @@ +import pygame +import sys +import gc + +from config import * +from Speller import SSVEPStimulus, SpellerGrid +from Speller import menu + + +# 1. PyGame setup +pygame.init() + +# 2. Ustawienia ekranu +screen = pygame.display.set_mode((WIDTH, HEIGHT), vsync=1) +pygame.display.set_caption("SSVEP Speller Experiment") + +# 3. Zegar +clock = pygame.time.Clock() + +# 4. Tworzymy obiekt +grid = SpellerGrid((WIDTH, HEIGHT), FREQS, ALPHABET_TREE["root"]) + +state = "MENU" + +running = True + +while running: + events = pygame.event.get() + + for event in events: + gc.collect() + + if event.type == pygame.QUIT: + running = False + + if state == "MENU" and event.type == pygame.KEYDOWN: + if event.key == pygame.K_1: + gc.disable() + + state = "OFFLINE" + print("Startujemy fazę kalibracji...") + + if event.key == pygame.K_2: + gc.disable() + + state = "ONLINE" + print("Startujemy Speller...") + + + screen.fill((0, 0, 0)) + + if state == "MENU": + menu.draw_menu(screen) + + if state == "OFFLINE": + grid.update() + grid.draw(screen) + + if state == "ONLINE": + grid.update() + grid.draw(screen) + + pygame.display.flip() + + clock.tick(60) + +pygame.quit() +sys.exit() \ No newline at end of file