Skip to content

Commit 591a209

Browse files
committed
Magic Book Rendering works with various delays
1 parent cbb0b20 commit 591a209

File tree

12 files changed

+660
-0
lines changed

12 files changed

+660
-0
lines changed

Magic_AI_Storybook/book.py

Lines changed: 381 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,381 @@
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
57.1 KB
Binary file not shown.
66.8 KB
Binary file not shown.
23.9 KB
Loading
23.5 KB
Loading

Magic_AI_Storybook/images/loading.png

761 KB
Loading
589 KB
Loading

Magic_AI_Storybook/images/welcome.png

747 KB
Loading

0 commit comments

Comments
 (0)