from PIL import Image, ImageDraw class LoadingView: """Loading screen with animated JP logo. Circle matches CircleView.""" SETUP_FRAMES = 50 ANIMATION_FRAMES = 100 # ~1s at 50fps HOLD_FRAMES = 100 # hold completed logo ~9s (total ~10s) # SVG cubic bezier segments (absolute coordinates in 500x500 viewport) # P path: starts at center, goes up and curves around the bump _P_SEGMENTS = [ ((248.462, 250.406), (248.125, 213.074), (246.529, 178.548), (246.529, 178.548)), ((246.529, 178.548), (250.153, 120.959), (302.781, 109.091), (316.648, 109.906)), ((316.648, 109.906), (384.748, 113.909), (403.0, 185.637), (372.292, 222.732)), ] # J path: starts at bottom-left hook, ends at center _J_SEGMENTS = [ ((113.29, 351.138), (138.503, 404.984), (237.45, 410.427), (246.53, 320.295)), ((246.53, 320.295), (248.338, 302.355), (248.69, 275.888), (248.466, 249.666)), ] def __init__(self, size, center, radius_offset=11): self.size = size self.center = center self.target_radius = min(center) - radius_offset self.frame = 0 self.total_frames = self.SETUP_FRAMES + self.ANIMATION_FRAMES + self.HOLD_FRAMES # Transform SVG coords → display coords scale = self.target_radius / 240.0 svg_cx, svg_cy = 252.565, 249.335 dx = self.center[0] - svg_cx * scale dy = self.center[1] - svg_cy * scale self.p_points = self._sample_path(self._P_SEGMENTS, scale, dx, dy) self.j_points = self._sample_path(self._J_SEGMENTS, scale, dx, dy) @staticmethod def _sample_path(segments, scale, dx, dy, samples_per_segment=30): points = [] for p0, p1, p2, p3 in segments: for i in range(samples_per_segment + 1): if i == 0 and points: continue t = i / samples_per_segment u = 1 - t x = u**3*p0[0] + 3*u**2*t*p1[0] + 3*u*t**2*p2[0] + t**3*p3[0] y = u**3*p0[1] + 3*u**2*t*p1[1] + 3*u*t**2*p2[1] + t**3*p3[1] points.append((x * scale + dx, y * scale + dy)) return points @staticmethod def _ease_in_out(t): if t < 0.5: return 2 * t * t return 1 - (-2 * t + 2) ** 2 / 2 def get_frame(self): """Return (PIL Image, is_done) advancing the animation by one frame.""" if self.frame >= self.total_frames: return self._render(1.0), True if self.frame >= self.SETUP_FRAMES + self.ANIMATION_FRAMES: progress = 1.0 elif self.frame < self.SETUP_FRAMES: progress = 0.0 else: progress = self._ease_in_out((self.frame - self.SETUP_FRAMES) / self.ANIMATION_FRAMES) im = self._render(progress) self.frame += 1 return im, False def _render(self, progress): im = Image.new('1', self.size, 'white') draw = ImageDraw.Draw(im) # Circle outline — same as CircleView draw.circle(self.center, self.target_radius, outline='black') # P path: reveal from start forward (center appears first, extends toward bump) p_count = int(progress * len(self.p_points)) if p_count > 1: pts = self.p_points[:p_count] for i in range(len(pts) - 1): draw.line([pts[i], pts[i + 1]], fill='black', width=2) # J path: reveal from end backward (center appears first, extends toward hook) j_count = int(progress * len(self.j_points)) if j_count > 1: start = len(self.j_points) - j_count pts = self.j_points[start:] for i in range(len(pts) - 1): draw.line([pts[i], pts[i + 1]], fill='black', width=2) return im