Rsan0948
Prepare for open source release v1.0.0
de8ccff
Raw
History Blame Contribute Delete
14 kB
"""Slide element classes: base class, all element types, and Slide container."""
import pygame
from ._drawing import _parse_color
from ._enums import Anchor, EntryAnim, _parse_anchor, _parse_entry, _parse_transition
class SlideElement:
"""Base class for all slide elements."""
def __init__(self, pos=(0, 0), anchor="top_left", z_order=0,
entry_anim="none", anim_delay=0.0, anim_duration=0.5,
visible=True, opacity=1.0, name=None, group=None, **kwargs):
# Friendly aliases for host integrations.
if "anim" in kwargs:
entry_anim = kwargs.pop("anim")
if "delay" in kwargs:
anim_delay = kwargs.pop("delay")
if "dur" in kwargs:
anim_duration = kwargs.pop("dur")
self.pos = pos # normalized (x, y)
self.anchor = _parse_anchor(anchor)
self.z_order = z_order
self.entry_anim = _parse_entry(entry_anim)
self.anim_delay = anim_delay
self.anim_duration = max(0.05, anim_duration)
self.visible = visible
self.opacity = opacity
self.name = name
self.group = group
# runtime state
self._anim_progress = 0.0 if self.entry_anim != EntryAnim.NONE else 1.0
self._started = False
def _resolve_rect(self, elem_w, elem_h, slide_w, slide_h):
px = self.pos[0] * slide_w
py = self.pos[1] * slide_h
a = self.anchor
if a in (Anchor.TOP_CENTER, Anchor.CENTER, Anchor.BOTTOM_CENTER):
px -= elem_w / 2
elif a in (Anchor.TOP_RIGHT, Anchor.CENTER_RIGHT, Anchor.BOTTOM_RIGHT):
px -= elem_w
if a in (Anchor.CENTER_LEFT, Anchor.CENTER, Anchor.CENTER_RIGHT):
py -= elem_h / 2
elif a in (Anchor.BOTTOM_LEFT, Anchor.BOTTOM_CENTER, Anchor.BOTTOM_RIGHT):
py -= elem_h
return px, py
def _ease(self, t):
# ease-out cubic
return 1 - (1 - t) ** 3
def _apply_anim(self, surface, dest_x, dest_y, slide_w, slide_h, progress):
p = self._ease(progress)
anim = self.entry_anim
alpha = int(255 * self.opacity * (p if anim in (
EntryAnim.FADE_IN, EntryAnim.ZOOM_IN,
EntryAnim.SLIDE_IN_LEFT, EntryAnim.SLIDE_IN_RIGHT,
EntryAnim.SLIDE_IN_UP, EntryAnim.SLIDE_IN_DOWN
) else self.opacity))
alpha = max(0, min(255, alpha))
ox, oy = 0, 0
if anim == EntryAnim.SLIDE_IN_LEFT:
ox = -(1 - p) * slide_w * 0.15
elif anim == EntryAnim.SLIDE_IN_RIGHT:
ox = (1 - p) * slide_w * 0.15
elif anim == EntryAnim.SLIDE_IN_UP:
oy = -(1 - p) * slide_h * 0.1
elif anim == EntryAnim.SLIDE_IN_DOWN:
oy = (1 - p) * slide_h * 0.1
if anim == EntryAnim.ZOOM_IN and p < 1.0:
scale = 0.7 + 0.3 * p
new_w = int(surface.get_width() * scale)
new_h = int(surface.get_height() * scale)
if new_w > 0 and new_h > 0:
surface = pygame.transform.smoothscale(surface, (new_w, new_h))
ox += (surface.get_width() - new_w) / 2 * (1 - p)
oy += (surface.get_height() - new_h) / 2 * (1 - p)
if alpha < 255:
surface.set_alpha(alpha)
return surface, dest_x + ox, dest_y + oy
class TextBox(SlideElement):
"""A text element with full styling control."""
def __init__(self, text="", pos=(0, 0), anchor="top_left",
style="body", color=None, font_size=None,
bold=None, italic=False, underline=False,
width=None, line_spacing=1.4,
align="left", bg_color=None, bg_radius=8,
bg_padding=(10, 8), bg_pad=None, shadow=False, border_color=None,
border_width=2, bullet=None, bullet_indent=0.02,
max_lines=None, z_order=0, entry_anim="none",
anim_delay=0.0, anim_duration=0.5, opacity=1.0,
name=None, group=None, visible=True):
if bg_pad is not None:
bg_padding = bg_pad
if isinstance(bg_padding, (int, float)):
bg_padding = (bg_padding, bg_padding)
super().__init__(pos, anchor, z_order, entry_anim, anim_delay,
anim_duration, visible, opacity, name, group)
self.text = text
self.style = style
self.color = _parse_color(color) if color else None
self.font_size = font_size # override; absolute pixels or ratio if < 1
self.bold = bold
self.italic = italic
self.underline = underline
self.width = width # normalized width for wrapping
self.line_spacing = line_spacing
self.align = align
self.bg_color = _parse_color(bg_color) if bg_color else None
self.bg_radius = bg_radius
self.bg_padding = bg_padding if isinstance(bg_padding, (list, tuple)) else (bg_padding, bg_padding)
self.shadow = shadow
self.border_color = _parse_color(border_color) if border_color else None
self.border_width = border_width
self.bullet = bullet
self.bullet_indent = bullet_indent
self.max_lines = max_lines
class ImageBox(SlideElement):
"""An image element loaded from file path or pygame Surface."""
def __init__(self, source=None, pos=(0, 0), anchor="top_left",
size=None, maintain_aspect=True,
border_radius=0, border_color=None, border_width=2,
shadow=True, opacity=1.0, tint=None,
z_order=0, entry_anim="none",
anim_delay=0.0, anim_duration=0.5,
name=None, group=None, visible=True):
super().__init__(pos, anchor, z_order, entry_anim, anim_delay,
anim_duration, visible, opacity, name, group)
self.source = source # file path or pygame.Surface
self.size = size # (norm_w, norm_h) or None
self.maintain_aspect = maintain_aspect
self.border_radius = border_radius
self.border_color = _parse_color(border_color) if border_color else None
self.border_width = border_width
self.shadow = shadow
self.tint = _parse_color(tint) if tint else None
self._cached_surface = None
self._cached_key = None
class Shape(SlideElement):
"""A geometric shape: rect, rounded_rect, circle, ellipse, line, polygon."""
def __init__(self, shape_type="rect", pos=(0, 0), size=(0.1, 0.1),
anchor="top_left", color="#ffffff", fill=True,
border_color=None, border_width=2, radius=12,
shadow=False, opacity=1.0,
points=None, # for polygon/line: list of (norm_x, norm_y)
gradient=None, # (color_top, color_bottom)
z_order=0, entry_anim="none",
anim_delay=0.0, anim_duration=0.5,
name=None, group=None, visible=True):
super().__init__(pos, anchor, z_order, entry_anim, anim_delay,
anim_duration, visible, opacity, name, group)
self.shape_type = shape_type
self.size = size
self.color = _parse_color(color)
self.fill = fill
self.border_color = _parse_color(border_color) if border_color else None
self.border_width = border_width
self.radius = radius
self.shadow = shadow
self.gradient = gradient
self.points = points
class DynamicGraphic(SlideElement):
"""A dynamic/animated element rendered by a user callback.
The callback signature is:
def render(surface, rect, dt, elapsed, theme, **kwargs)
where:
surface = pygame surface to draw on (full slide size)
rect = (x, y, w, h) pixel rect allocated for this element
dt = delta time since last frame
elapsed = total time on this slide
theme = current theme dict
"""
def __init__(self, render_fn=None, pos=(0, 0), size=(0.3, 0.3),
anchor="top_left", z_order=0, entry_anim="none",
anim_delay=0.0, anim_duration=0.5, opacity=1.0,
name=None, group=None, visible=True, user_data=None):
super().__init__(pos, anchor, z_order, entry_anim, anim_delay,
anim_duration, visible, opacity, name, group)
self.render_fn = render_fn
self.size = size
self.user_data = user_data or {}
class BulletList(SlideElement):
"""Convenience element: a styled bullet-point list."""
def __init__(self, items=None, pos=(0, 0), anchor="top_left",
style="body", color=None, bullet_char="●",
bullet_color=None, indent=0.03, item_spacing=1.6,
width=None, font_size=None, bold=None,
z_order=0, entry_anim="none", stagger_delay=0.15,
anim_delay=0.0, anim_duration=0.4, opacity=1.0,
name=None, group=None, visible=True,
sub_bullet_char="○", nested_indent=0.025):
super().__init__(pos, anchor, z_order, entry_anim, anim_delay,
anim_duration, visible, opacity, name, group)
self.items = items or []
self.style = style
self.color = _parse_color(color) if color else None
self.bullet_char = bullet_char
self.bullet_color = _parse_color(bullet_color) if bullet_color else None
self.indent = indent
self.item_spacing = item_spacing
self.width = width
self.font_size = font_size
self.bold = bold
self.stagger_delay = stagger_delay
self.sub_bullet_char = sub_bullet_char
self.nested_indent = nested_indent
class CodeBlock(SlideElement):
"""A syntax-highlighted (styled) code block."""
def __init__(self, code="", pos=(0, 0), anchor="top_left",
width=None, height=None, font_size=None,
bg_color=None, text_color=None,
border_radius=10, shadow=True, line_numbers=False,
z_order=0, entry_anim="none",
anim_delay=0.0, anim_duration=0.5, opacity=1.0,
name=None, group=None, visible=True,
title=None, title_color=None):
super().__init__(pos, anchor, z_order, entry_anim, anim_delay,
anim_duration, visible, opacity, name, group)
self.code = code
self.width = width
self.height = height
self.font_size = font_size
self.bg_color = _parse_color(bg_color) if bg_color else None
self.text_color = _parse_color(text_color) if text_color else None
self.border_radius = border_radius
self.shadow = shadow
self.line_numbers = line_numbers
self.title = title
self.title_color = _parse_color(title_color) if title_color else None
class ProgressBar(SlideElement):
"""A progress/percentage bar."""
def __init__(self, value=0.5, pos=(0, 0), anchor="top_left",
size=(0.3, 0.02), bg_color=None, fill_color=None,
border_radius=6, label=None, label_color=None,
animated=True, z_order=0, entry_anim="none",
anim_delay=0.0, anim_duration=0.8, opacity=1.0,
name=None, group=None, visible=True):
super().__init__(pos, anchor, z_order, entry_anim, anim_delay,
anim_duration, visible, opacity, name, group)
self.value = value
self.size = size
self.bg_color = _parse_color(bg_color) if bg_color else None
self.fill_color = _parse_color(fill_color) if fill_color else None
self.border_radius = border_radius
self.label = label
self.label_color = _parse_color(label_color) if label_color else None
self.animated = animated
class Divider(SlideElement):
"""A horizontal or vertical divider line."""
def __init__(self, pos=(0, 0), length=0.9, thickness=2,
color=None, orientation="horizontal",
anchor="top_left", z_order=0, entry_anim="none",
anim_delay=0.0, anim_duration=0.3, opacity=0.5,
name=None, group=None, visible=True,
gradient_fade=True):
super().__init__(pos, anchor, z_order, entry_anim, anim_delay,
anim_duration, visible, opacity, name, group)
self.length = length
self.thickness = thickness
self.color = _parse_color(color) if color else None
self.orientation = orientation
self.gradient_fade = gradient_fade
class Slide:
"""A single slide containing elements."""
def __init__(self, transition="fade", transition_duration=0.4,
bg_color=None, bg_gradient=None, bg_image=None,
accent_bar=True, accent_bar_pos="bottom",
accent_bar_thickness=4, slide_number=True,
footer_text=None, on_enter=None, on_exit=None,
duration=None, auto_advance=False, name=None):
self.elements: list[SlideElement] = []
self.transition = _parse_transition(transition)
self.transition_duration = transition_duration
self.bg_color = _parse_color(bg_color) if bg_color else None
self.bg_gradient = bg_gradient
self.bg_image = bg_image
self.accent_bar = accent_bar
self.accent_bar_pos = accent_bar_pos
self.accent_bar_thickness = accent_bar_thickness
self.slide_number = slide_number
self.footer_text = footer_text
self.on_enter = on_enter
self.on_exit = on_exit
self.duration = duration
self.auto_advance = auto_advance
self.name = name
self._elapsed = 0.0
def add(self, element: SlideElement):
self.elements.append(element)
return self # chainable
def add_all(self, *elements):
for e in elements:
self.elements.append(e)
return self