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