import pygame
import sys
import math
# Initialize Pygame
pygame.init()
pygame.font.init()
# Constants
WIDTH, HEIGHT = 1100, 780
SCREEN = pygame.display.set_mode((WIDTH, HEIGHT))
pygame.display.set_caption("Professional Microtype Engine & Layout Simulator")
CLOCK = pygame.time.Clock()
# Palettes
COLOR_BG = (249, 248, 245) # Premium archival paper
COLOR_TEXT = (35, 35, 35) # Soft black
COLOR_MARGIN = (230, 90, 90) # Margin guideline
COLOR_UI_BG = (225, 227, 230)
COLOR_UI_TEXT = (50, 55, 60)
COLOR_SLIDER = (70, 130, 180)
COLOR_ACTIVE = (46, 139, 87) # SeaGreen for scores/active selections
# Load fonts
try:
FONT_SIZE = 18
FONT = pygame.font.SysFont("georgia", FONT_SIZE)
FONT_BOLD = pygame.font.SysFont("georgia", FONT_SIZE, bold=True)
except:
FONT = pygame.font.Font(None, FONT_SIZE)
FONT_BOLD = pygame.font.Font(None, FONT_SIZE)
SAMPLE_TEXT = (
"Typography is the art and technique of arranging type to make written language "
"legible, readable, and appealing when displayed. The Knuth-Plass dynamic programming "
"algorithm revolutionizes this by looking ahead at the entire paragraph. Instead of "
"making hasty choices on a line-by-line basis, it distributes layout 'badness' evenly, "
"preventing unexpected blocks of loose text. Combined with microtype tracking expansions, "
"subtle margin protrusions yield pristine geometric columns resembling classic elite print."
)
# --- UI Widgets ---
class Slider:
def __init__(self, x, y, w, h, min_val, max_val, start_val, label):
self.rect = pygame.Rect(x, y, w, h)
self.min_val = min_val
self.max_val = max_val
self.val = start_val
self.label = label
self.grabbed = False
self.update_handle()
def update_handle(self):
ratio = (self.val - self.min_val) / (self.max_val - self.min_val)
hx = self.rect.x + int(ratio * self.rect.w)
self.handle_rect = pygame.Rect(hx - 5, self.rect.y - 4, 10, self.rect.h + 8)
def draw(self, screen):
lbl = FONT.render(f"{self.label}: {self.val:.2f}", True, COLOR_UI_TEXT)
screen.blit(lbl, (self.rect.x, self.rect.y - 22))
pygame.draw.rect(screen, (190, 195, 200), self.rect, border_radius=3)
pygame.draw.rect(screen, COLOR_SLIDER, self.handle_rect, border_radius=3)
def handle_event(self, event):
if event.type == pygame.MOUSEBUTTONDOWN:
if self.handle_rect.collidepoint(event.pos) or self.rect.collidepoint(event.pos):
self.grabbed = True
elif event.type == pygame.MOUSEBUTTONUP:
self.grabbed = False
elif event.type == pygame.MOUSEMOTION and self.grabbed:
mx = max(self.rect.x, min(event.pos[0], self.rect.x + self.rect.w))
rel = (mx - self.rect.x) / self.rect.w
self.val = self.min_val + rel * (self.max_val - self.min_val)
self.update_handle()
class RadioSelector:
def __init__(self, x, y, options):
self.x = x
self.y = y
self.options = options
self.selected_index = 1 # Default to Knuth-Plass
self.buttons = []
for idx, opt in enumerate(options):
bx = x + (idx * 280)
self.buttons.append(pygame.Rect(bx, y, 20, 20))
def draw(self, screen):
lbl_title = FONT_BOLD.render("Line Breaking Algorithm:", True, COLOR_UI_TEXT)
screen.blit(lbl_title, (self.x, self.y - 25))
for idx, opt in enumerate(self.options):
rect = self.buttons[idx]
# Draw outer circle
pygame.draw.circle(screen, COLOR_UI_TEXT, rect.center, 10, 2)
# Draw internal selection
if idx == self.selected_index:
pygame.draw.circle(screen, COLOR_ACTIVE, rect.center, 6)
lbl = FONT.render(opt, True, COLOR_UI_TEXT)
screen.blit(lbl, (rect.x + 25, rect.y + 1))
def handle_event(self, event):
if event.type == pygame.MOUSEBUTTONDOWN:
for idx, rect in enumerate(self.buttons):
# Expanded click zone for user convenience
click_zone = rect.inflate(150, 10)
if click_zone.collidepoint(event.pos):
self.selected_index = idx
return True
return False
# --- Helper Text Calculation Tools ---
def compute_word_widths(words, font, tracking):
return [sum(font.size(char)[0] + tracking for char in word) for word in words]
def calc_line_badness(width, test_width, num_gaps, base_space_width, min_space, max_space, ideal_space, is_last=False):
if num_gaps == 0:
remaining = width - test_width
return (remaining ** 2) if remaining >= 0 else 500000
actual_space = (width - test_width) / num_gaps
if actual_space < min_space:
# Heavily penalize over-compressed lines
return 100000 + (min_space - actual_space) * 50000
elif actual_space > max_space:
# Loose lines
return int(((actual_space - max_space) ** 2) * 500)
else:
# Standard deviation penalty
badness = int(((actual_space - ideal_space) ** 2) * 100)
if is_last and actual_space > ideal_space:
return 0 # Last line of a paragraph shouldn't stretch to fill the margin
return badness
def apply_protrusion(word, font, protrusion):
protruding_chars = [".", ",", "-", "!", "?"]
if protrusion > 0 and word[-1:] in protruding_chars:
return font.size(word[-1:])[0] * protrusion * 0.5
return 0
# --- Line-Breaking Core Algorithms ---
def layout_greedy(words, word_widths, font, width, min_space, max_space, ideal_space, protrusion):
""" a) Traditional First-Fit Greedy Algorithm """
lines = []
current_line, current_widths = [], []
current_width = 0
for idx, word in enumerate(words):
w_width = word_widths[idx]
p_adjust = apply_protrusion(word, font, protrusion)
# Test if it fits with standard spaces
test_w = current_width + w_width + (ideal_space if current_line else 0) - p_adjust
if test_w <= width or not current_line:
current_line.append(word)
current_widths.append(w_width)
current_width += w_width + (ideal_space if len(current_line) > 1 else 0)
else:
# Seal line
num_gaps = len(current_line) - 1
last_word_pad = apply_protrusion(current_line[-1], font, protrusion)
pure_width = sum(current_widths)
space_used = (width - (pure_width - last_word_pad)) / num_gaps if num_gaps > 0 else ideal_space
badness = calc_line_badness(width, pure_width - last_word_pad, num_gaps, ideal_space, min_space, max_space, ideal_space)
lines.append((current_line, current_widths, space_used, False, badness))
current_line, current_widths = [word], [w_width]
current_width = w_width
if current_line:
num_gaps = len(current_line) - 1
last_word_pad = apply_protrusion(current_line[-1], font, protrusion)
pure_width = sum(current_widths)
space_used = ideal_space
badness = calc_line_badness(width, pure_width - last_word_pad, num_gaps, ideal_space, min_space, max_space, ideal_space, is_last=True)
lines.append((current_line, current_widths, space_used, True, badness))
return lines
def layout_knuth_plass(words, word_widths, font, width, min_space, max_space, ideal_space, protrusion):
""" b) Look-Ahead Optimization (Global Minimum Variance) """
n = len(words)
dp = [(float('inf'), -1, ideal_space, 0)] * (n + 1)
dp[0] = (0, -1, ideal_space, 0)
for i in range(n):
if dp[i][0] == float('inf'): continue
current_width = 0
for j in range(i, n):
current_width += word_widths[j]
num_gaps = j - i
is_last = (j == n - 1)
p_adjust = apply_protrusion(words[j], font, protrusion)
line_txt_w = current_width - p_adjust
badness = calc_line_badness(width, line_txt_w, num_gaps, ideal_space, min_space, max_space, ideal_space, is_last)
actual_space = ideal_space
if num_gaps > 0 and not is_last:
actual_space = (width - line_txt_w) / num_gaps
p_cost = dp[i][0] + badness
if p_cost < dp[j + 1][0]:
dp[j + 1] = (p_cost, i, actual_space, badness)
lines, curr = [], n
while curr > 0:
parent = dp[curr][1]
if parent == -1: break
is_last = (curr == n)
lines.append((words[parent:curr], word_widths[parent:curr], dp[curr][2], is_last, dp[curr][3]))
curr = parent
lines.reverse()
return lines
def layout_first_fit_tight(words, word_widths, font, width, min_space, max_space, ideal_space, protrusion):
""" c) Alternating Minimum Space Greedy Algorithm """
# This variant forces as many words onto the line as physically allowed by compressing down to min_space limits.
lines = []
current_line, current_widths = [], []
for idx, word in enumerate(words):
w_width = word_widths[idx]
current_line.append(word)
current_widths.append(w_width)
p_adjust = apply_protrusion(word, font, protrusion)
num_gaps = len(current_line) - 1
min_needed = sum(current_widths) + (num_gaps * min_space) - p_adjust
if min_needed > width and num_gaps > 0:
# Overfilled line, dump the last token to the next row
popped_word = current_line.pop()
popped_width = current_widths.pop()
num_gaps = len(current_line) - 1
last_word_pad = apply_protrusion(current_line[-1], font, protrusion)
pure_width = sum(current_widths)
space_used = (width - (pure_width - last_word_pad)) / num_gaps if num_gaps > 0 else ideal_space
badness = calc_line_badness(width, pure_width - last_word_pad, num_gaps, ideal_space, min_space, max_space, ideal_space)
lines.append((current_line, current_widths, space_used, False, badness))
current_line, current_widths = [popped_word], [popped_width]
if current_line:
num_gaps = len(current_line) - 1
last_word_pad = apply_protrusion(current_line[-1], font, protrusion)
pure_width = sum(current_widths)
badness = calc_line_badness(width, pure_width - last_word_pad, num_gaps, ideal_space, min_space, max_space, ideal_space, is_last=True)
lines.append((current_line, current_widths, ideal_space, True, badness))
return lines
# --- Rendering ---
def render_paragraph(lines, font, x_start, y_start, tracking, protrusion, leading_ratio):
y = y_start
line_height = int(font.get_linesize() * leading_ratio)
for line_words, line_widths, space_width, is_last, _ in lines:
x = x_start
num_words = len(line_words)
for w_idx, word in enumerate(line_words):
for c_idx, char in enumerate(word):
char_surf = font.render(char, True, COLOR_TEXT)
render_x = x
if w_idx == num_words - 1 and c_idx == len(word) - 1:
render_x += apply_protrusion(word, font, protrusion)
SCREEN.blit(char_surf, (render_x, y))
x += char_surf.get_width() + tracking
if w_idx < num_words - 1:
x += space_width
y += line_height
# --- UI Layout ---
sliders = [
Slider(50, 540, 260, 10, -1.5, 3.0, 0.0, "Font Expansion (Tracking)"),
Slider(380, 540, 260, 10, 0.4, 1.0, 0.65, "Min Word Space Elasticity"),
Slider(710, 540, 260, 10, 1.0, 3.0, 1.70, "Max Word Space Elasticity"),
Slider(50, 620, 260, 10, 0.0, 1.2, 0.5, "Character Protrusion"),
Slider(380, 620, 260, 10, 0.8, 2.5, 1.3, "Line Height (Leading)")
]
algo_radio = RadioSelector(50, 710, ["a) Greedy Algorithm", "b) Knuth-Plass Ahead", "c) Space-Tight Fit"])
MARGIN_LEFT = 200
BOX_WIDTH = 700
# Main loop
while True:
SCREEN.fill(COLOR_BG)
# Event Engine Loop
for event in pygame.event.get():
if event.type == pygame.QUIT:
pygame.quit()
sys.exit()
for slider in sliders:
slider.handle_event(event)
algo_radio.handle_event(event)
# Drawing background infrastructure boundaries
pygame.draw.rect(SCREEN, COLOR_UI_BG, (0, 480, WIDTH, HEIGHT - 480))
pygame.draw.line(SCREEN, (190, 195, 200), (0, 480), (WIDTH, 480), 2)
pygame.draw.line(SCREEN, COLOR_MARGIN, (MARGIN_LEFT, 75), (MARGIN_LEFT, 450), 1)
pygame.draw.line(SCREEN, COLOR_MARGIN, (MARGIN_LEFT + BOX_WIDTH, 75), (MARGIN_LEFT + BOX_WIDTH, 450), 1)
# Gather metrics
base_space_width = FONT.size(" ")[0]
tracking_val = sliders[0].val
min_space = base_space_width * sliders[1].val
max_space = base_space_width * sliders[2].val
protrusion_val = sliders[3].val
leading_val = sliders[4].val
# Re-tokenize and check widths inside runtime
words = SAMPLE_TEXT.split(" ")
word_widths = compute_word_widths(words, FONT, tracking_val)
# Route processing via radio flag selections
if algo_radio.selected_index == 0:
computed_lines = layout_greedy(words, word_widths, FONT, BOX_WIDTH, min_space, max_space, base_space_width, protrusion_val)
elif algo_radio.selected_index == 1:
computed_lines = layout_knuth_plass(words, word_widths, FONT, BOX_WIDTH, min_space, max_space, base_space_width, protrusion_val)
else:
computed_lines = layout_first_fit_tight(words, word_widths, FONT, BOX_WIDTH, min_space, max_space, base_space_width, protrusion_val)
# Cumulative Badness Score Calculation
total_paragraph_badness = sum(line[4] for line in computed_lines)
# Render Paragraph Blocks
render_paragraph(computed_lines, FONT, MARGIN_LEFT, 95, tracking_val, protrusion_val, leading_val)
# Render Widgets
for slider in sliders:
slider.draw(SCREEN)
algo_radio.draw(SCREEN)
# Display Badness score at the top panel
score_lbl = FONT_BOLD.render(f"Overall Paragraph Badness Score: {total_paragraph_badness}", True, COLOR_ACTIVE)
SCREEN.blit(score_lbl, (MARGIN_LEFT, 35))
pygame.display.flip()
CLOCK.tick(30)

