1+ import sys
2+ import os
3+ import time
4+ from enum import Enum
5+ import pygame
6+
7+ # Image Names
8+ WELCOME_IMAGE = 'welcome.png'
9+ BACKGROUND_IMAGE = 'paper_background.png'
10+ LOADING_IMAGE = 'loading.png'
11+ BUTTON_BACK_IMAGE = 'button_back.png'
12+ BUTTON_NEXT_IMAGE = 'button_next.png'
13+
14+ # Asset Paths
15+ IMAGES_PATH = os .path .dirname (sys .argv [0 ]) + 'images/'
16+ FONTS_PATH = os .path .dirname (sys .argv [0 ]) + 'fonts/'
17+
18+ # Font Path, Size
19+ TITLE_FONT = (FONTS_PATH + "lucida_black.ttf" , 48 )
20+ TITLE_COLOR = (0 , 0 , 0 )
21+ TEXT_FONT = (FONTS_PATH + "times new roman.ttf" , 24 )
22+ TEXT_COLOR = (0 , 0 , 0 )
23+
24+ # Delays to control the speed of the text
25+ # Default
26+ CHARACTER_DELAY = 0.03
27+ WORD_DELAY = 0.2
28+ SENTENCE_DELAY = 1
29+ PARAGRAPH_DELAY = 2
30+
31+ # Letter by Letter
32+ #CHARACTER_DELAY = 0.1
33+ #WORD_DELAY = 0
34+ #SENTENCE_DELAY = 0
35+ #PARAGRAPH_DELAY = 0
36+
37+ # Word by Word
38+ #CHARACTER_DELAY = 0
39+ #WORD_DELAY = 0.3
40+ #SENTENCE_DELAY = 0.5
41+ #PARAGRAPH_DELAY = 0
42+
43+ # No Delays
44+ #CHARACTER_DELAY = 0
45+ #WORD_DELAY = 0
46+ #SENTENCE_DELAY = 0
47+ #PARAGRAPH_DELAY = 0
48+
49+
50+ # Whitespace Settings in Pixels
51+ PAGE_TOP_MARGIN = 20
52+ PAGE_SIDE_MARGIN = 20
53+ PAGE_BOTTOM_MARGIN = 0
54+ PAGE_NAV_HEIGHT = 100
55+ EXTRA_LINE_SPACING = 0
56+ PARAGRAPH_SPACING = 30
57+
58+ class Position (Enum ):
59+ TOP = 0
60+ CENTER = 1
61+ BOTTOM = 2
62+ LEFT = 3
63+ RIGHT = 4
64+
65+ class Button :
66+ def __init__ (self , x , y , image , action ):
67+ self .x = x
68+ self .y = y
69+ self .image = image
70+ self .action = action
71+ self ._width = self .image .get_width ()
72+ self ._height = self .image .get_height ()
73+
74+ def is_in_bounds (self , x , y ):
75+ return self .x <= x <= self .x + self .width and self .y <= y <= self .y + self .height
76+
77+ def is_pressed (self ):
78+ pass
79+
80+ @property
81+ def width (self ):
82+ return self ._width
83+
84+ @property
85+ def height (self ):
86+ return self ._height
87+
88+ class Textarea :
89+ def __init__ (self , x , y , width , height ):
90+ self .x = x
91+ self .y = y
92+ self .width = width
93+ self .height = height
94+
95+ @property
96+ def size (self ):
97+ return {
98+ "width" : self .width ,
99+ "height" : self .height
100+ }
101+
102+ class Book :
103+ def __init__ (self , rotation = 0 ):
104+ self .paragraph_number = 0
105+ self .page = 0
106+ self .title = ""
107+ self .paragraphs = []
108+ self .pages = []
109+ self .rotation = rotation
110+ self .images = {}
111+ self .fonts = {}
112+ self .width = 0
113+ self .height = 0
114+ self .back_button = None
115+ self .next_button = None
116+
117+ def init (self ):
118+ # Output to the LCD instead of the console
119+ os .putenv ('DISPLAY' , ':0' )
120+
121+ # Initialize the display
122+ pygame .init ()
123+ self .screen = pygame .display .set_mode ((0 ,0 ), pygame .FULLSCREEN )
124+ self .width = self .screen .get_height ()
125+ self .height = self .screen .get_width ()
126+
127+ # Preload images
128+ self .load_image ("welcome" , WELCOME_IMAGE )
129+ self .load_image ("background" , BACKGROUND_IMAGE )
130+ self .load_image ("loading" , LOADING_IMAGE )
131+
132+ # Preload fonts
133+ self .load_font ("title" , TITLE_FONT )
134+ self .load_font ("text" , TEXT_FONT )
135+
136+ # Add buttons
137+ back_button_image = pygame .image .load (IMAGES_PATH + BUTTON_BACK_IMAGE )
138+ next_button_image = pygame .image .load (IMAGES_PATH + BUTTON_NEXT_IMAGE )
139+ button_spacing = (self .width - (back_button_image .get_width () + next_button_image .get_width ())) // 3
140+ button_ypos = self .height - PAGE_NAV_HEIGHT + (PAGE_NAV_HEIGHT - next_button_image .get_height ()) // 2
141+ self .back_button = Button (
142+ button_spacing ,
143+ button_ypos ,
144+ back_button_image ,
145+ self .previous_page
146+ )
147+ self .next_button = Button (
148+ self .width - button_spacing - next_button_image .get_width (),
149+ button_ypos ,
150+ next_button_image ,
151+ self .next_page
152+ )
153+
154+ # Add Text Area
155+ self .textarea = Textarea (
156+ PAGE_SIDE_MARGIN ,
157+ PAGE_TOP_MARGIN ,
158+ self .width - PAGE_SIDE_MARGIN * 2 ,
159+ self .height - PAGE_NAV_HEIGHT - PAGE_TOP_MARGIN - PAGE_BOTTOM_MARGIN
160+ )
161+
162+ pygame .mouse .set_visible (False )
163+ self .screen .fill ((255 ,255 ,255 ))
164+
165+ def handle_events (self ):
166+ for event in pygame .event .get ():
167+ if event .type == pygame .QUIT :
168+ raise SystemExit
169+ elif event .type == pygame .MOUSEBUTTONDOWN :
170+ if event .button == 1 :
171+ # If clicked in text area and book is still rendering, skip to the end
172+ print (f"Left mouse button pressed at { event .pos } " )
173+ # If button pressed while visible, trigger action
174+ elif event .type == pygame .MOUSEBUTTONUP :
175+ # Not sure if we will need this
176+ print ("Mouse button has been released" )
177+
178+ def add_page (self , paragraph = 0 , word = 0 ):
179+ # Add rendered page information to make flipping between them easier
180+ self .pages .append ({
181+ "paragraph" : paragraph ,
182+ "word" : word ,
183+ })
184+
185+ def load_image (self , name , filename ):
186+ try :
187+ image = pygame .image .load (IMAGES_PATH + filename )
188+ self .images [name ] = image
189+ except pygame .error :
190+ return None
191+
192+ def load_font (self , name , details ):
193+ self .fonts [name ] = pygame .font .Font (details [0 ], details [1 ])
194+
195+ def get_position (self , object , x , y ):
196+ if x == Position .CENTER :
197+ x = (self .width - object .get_width ()) // 2
198+ elif x == Position .RIGHT :
199+ x = self .width - object .get_width ()
200+ elif x == Position .LEFT :
201+ x = 0
202+ elif not isinstance (x , int ):
203+ raise ValueError ("Invalid x position" )
204+
205+ if y == Position .CENTER :
206+ y = (self .height - object .get_height ()) // 2
207+ elif y == Position .BOTTOM :
208+ y = self .height - object .get_height ()
209+ elif y == Position .TOP :
210+ y = 0
211+ elif not isinstance (y , int ):
212+ raise ValueError ("Invalid y position" )
213+
214+ return (x , y )
215+
216+ # Display a surface either positionally or with a specific x,y coordinate
217+ def display_image (self , image , x = Position .CENTER , y = Position .CENTER , surface = None ):
218+ buffer = pygame .Surface ((self .width , self .height ), pygame .SRCALPHA , 32 )
219+ buffer = buffer .convert_alpha ()
220+ buffer .blit (image , self .get_position (image , x , y ))
221+ if surface is None :
222+ buffer = pygame .transform .rotate (buffer , self .rotation )
223+ self .screen .blit (buffer , (0 , 0 ))
224+ else :
225+ surface .blit (buffer , (0 , 0 ))
226+
227+ def display_current_page (self ):
228+ # This will be easier if we create a surface and just rotate that before rendering it to the screen
229+
230+ self .display_image (self .images ["background" ], Position .CENTER , Position .CENTER )
231+ pygame .display .update ()
232+
233+ # Use a cursor to keep track of where we are on the page
234+ # These values are relative to the text area
235+ self .cursor = {
236+ "x" : 0 ,
237+ "y" : 0
238+ }
239+
240+ # Display the title
241+ if self .page == 0 :
242+ title = self .render_title ()
243+ self .display_image (title , self .cursor ["x" ] + self .textarea .x , self .cursor ["y" ] + self .textarea .y )
244+ pygame .display .update ()
245+ self .cursor ["y" ] += title .get_height () + PARAGRAPH_SPACING
246+ time .sleep (PARAGRAPH_DELAY )
247+
248+ self .display_page_text ()
249+
250+ # Display the navigation buttons
251+ if self .page > 0 :
252+ self .display_image (self .back_button .image , self .back_button .x , self .back_button .y )
253+
254+ # TODO: If we are on the last page, don't display the next button
255+ self .display_image (self .next_button .image , self .next_button .x , self .next_button .y )
256+ pygame .display .update ()
257+
258+ def render_character (self , character ):
259+ return self .fonts ["text" ].render (character , True , (0 , 0 , 0 ))
260+
261+ def display_page_text (self ):
262+ # TODO: We need an accurate way to determine when a previous page has already been added so we don't add it again
263+
264+ # Display the paragraphs, one paragraph at a time, one word at a time until we reach the end of the line
265+ # then move to the next line. Once we are at the end of the page, stop displaying paragraphs
266+ paragraph_number = self .pages [self .page ]["paragraph" ]
267+ word_number = self .pages [self .page ]["word" ]
268+
269+ # Display a paragraph at a time
270+ while self .paragraph_number < len (self .paragraphs ):
271+ paragraph = self .paragraphs [paragraph_number ]
272+ while word_number < len (paragraph ):
273+ word = paragraph [word_number ]
274+ # Check if there is enough space to display the word
275+ if self .cursor ["x" ] + self .fonts ["text" ].size (word )[0 ] > self .textarea .width :
276+ # If not, move to the next line
277+ self .cursor ["x" ] = 0
278+ self .cursor ["y" ] += self .fonts ["text" ].get_height () + EXTRA_LINE_SPACING
279+ # If we have reached the end of the page, stop displaying paragraphs
280+ if self .cursor ["y" ] + self .fonts ["text" ].get_height () > self .textarea .height :
281+ self .add_page (paragraph_number , word_number )
282+ return
283+
284+ # Display the word one character at a time
285+ for character in word :
286+ character_surface = self .render_character (character )
287+ self .display_image (character_surface , self .cursor ["x" ] + self .textarea .x , self .cursor ["y" ] + self .textarea .y )
288+ pygame .display .update ()
289+ self .cursor ["x" ] += character_surface .get_width () + 1
290+ if character != " " :
291+ time .sleep (CHARACTER_DELAY )
292+
293+ # Advance the cursor by a spaces width
294+ self .cursor ["x" ] += self .render_character (" " ).get_width () + 1
295+
296+ # Look at last character only to avoid long delays on stuff like "!!!" or "?!" or "..."
297+ if word [- 1 :] in ["." , "!" , "?" ]:
298+ time .sleep (SENTENCE_DELAY )
299+ else :
300+ time .sleep (WORD_DELAY )
301+ word_number += 1
302+
303+ # We have reached the end of the paragraph, so we need to move to the next line
304+ time .sleep (PARAGRAPH_DELAY )
305+ self .cursor ["x" ] = 0
306+ self .cursor ["y" ] += self .fonts ["text" ].get_height () + PARAGRAPH_SPACING
307+ word_number = 0
308+ paragraph_number += 1
309+
310+ # If we have reached the end of the page, stop displaying paragraphs
311+ if self .cursor ["y" ] + self .fonts ["text" ].get_height () > self .textarea .height :
312+ self .add_page (paragraph_number , word_number )
313+ return
314+
315+ def create_transparent_buffer (self , size ):
316+ if isinstance (size , (tuple , list )):
317+ (width , height ) = size
318+ elif isinstance (size , dict ):
319+ width = size ['width' ]
320+ height = size ['height' ]
321+ buffer = pygame .Surface ((width , height ), pygame .SRCALPHA , 32 )
322+ buffer = buffer .convert_alpha ()
323+ return buffer
324+
325+ def render_title (self ):
326+ # The title should be centered and wrapped if it is too wide for the screen
327+ buffer = self .create_transparent_buffer (self .textarea .size )
328+
329+ # Render the title as multiple lines if too big
330+ lines = self .wrap_text (self .title , self .fonts ["title" ], self .textarea .width )
331+ text_height = 0
332+ for line in lines :
333+ text = self .fonts ["title" ].render (line , True , TITLE_COLOR )
334+ buffer .blit (text , (buffer .get_width () // 2 - text .get_width () // 2 , text_height ))
335+ text_height += text .get_height ()
336+
337+ new_buffer = self .create_transparent_buffer ((self .textarea .width , text_height ))
338+ new_buffer .blit (buffer , (0 , 0 ))
339+
340+ return new_buffer
341+
342+ def wrap_text (self , text , font , width ):
343+ lines = []
344+ line = ""
345+ for word in text .split (" " ):
346+ if font .size (line + word )[0 ] < width :
347+ line += word + " "
348+ else :
349+ lines .append (line )
350+ line = word + " "
351+ lines .append (line )
352+ return lines
353+
354+ def previous_page (self ):
355+ if self .page > 0 :
356+ self .page -= 1
357+ self .display_current_page ()
358+
359+ def next_page (self ):
360+ if self .page < len (self .pages ) - 1 :
361+ self .page += 1
362+ self .display_current_page ()
363+
364+ def display_loading (self ):
365+ self .display_image (self .images ["loading" ], Position .CENTER , Position .CENTER )
366+ pygame .display .update ()
367+
368+ def display_welcome (self ):
369+ self .display_image (self .images ["welcome" ], Position .CENTER , Position .CENTER )
370+ pygame .display .update ()
371+
372+ # Parse out the title and story and separage into pages
373+ def parse_story (self , story ):
374+ self .title = story .split ("Title: " )[1 ].split ("\n \n " )[0 ]
375+ paragraphs = story .split ("\n \n " )[1 :]
376+ for paragraph in paragraphs :
377+ self .paragraphs .append (paragraph .split (" " ))
378+ self .add_page ()
379+
380+ # save settings
381+ # load settings
0 commit comments