112 lines
4.1 KiB
Python
112 lines
4.1 KiB
Python
from PIL import Image, ImageDraw
|
|
|
|
# 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)),
|
|
]
|
|
|
|
class LoadingView:
|
|
"""Loading screen with animated JP logo. Circle matches CircleView."""
|
|
SETUP_FRAMES = 100
|
|
ANIMATION_FRAMES = 100 # ~1s at 50fps
|
|
HOLD_FRAMES = 100 # hold completed logo ~9s (total ~10s)
|
|
|
|
|
|
im_size = (168, 144)
|
|
center = (168 // 2, 144 // 2)
|
|
|
|
TARGET_RADIUS = min(center) - 11 # 11px padding to match CircleView's radius_offset
|
|
SCALE = TARGET_RADIUS / 240.0
|
|
svg_cx, svg_cy = 252.565, 249.335
|
|
dx = center[0] - svg_cx * SCALE
|
|
dy = center[1] - svg_cy * SCALE
|
|
|
|
def __init__(self, size, center):
|
|
self.frame = 0
|
|
self.total_frames = self.SETUP_FRAMES + self.ANIMATION_FRAMES + self.HOLD_FRAMES
|
|
|
|
# Pre-render all frames at init time
|
|
self._cached_frames = FRAMES
|
|
|
|
@staticmethod
|
|
def _build_frame_cache():
|
|
frames = []
|
|
blank = LoadingView._render(0.0)
|
|
for i in range(LoadingView.SETUP_FRAMES):
|
|
frames.append(blank)
|
|
for i in range(LoadingView.ANIMATION_FRAMES):
|
|
progress = LoadingView._ease_in_out(i / LoadingView.ANIMATION_FRAMES)
|
|
frames.append(LoadingView._render(progress))
|
|
done = LoadingView._render(1.0)
|
|
for i in range(LoadingView.HOLD_FRAMES):
|
|
frames.append(done)
|
|
return frames
|
|
|
|
@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._cached_frames[-1], True
|
|
|
|
im = self._cached_frames[self.frame]
|
|
self.frame += 1
|
|
return im, False
|
|
|
|
@staticmethod
|
|
def _render(progress):
|
|
im = Image.new('1', LoadingView.im_size, 'white')
|
|
draw = ImageDraw.Draw(im)
|
|
|
|
# Circle outline — same as CircleView
|
|
draw.circle(LoadingView.center, LoadingView.TARGET_RADIUS, outline='black')
|
|
|
|
# P path: reveal from start forward (center appears first, extends toward bump)
|
|
p_count = int(progress * len(P_POINTS))
|
|
if p_count > 1:
|
|
pts = 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(J_POINTS))
|
|
if j_count > 1:
|
|
start = len(J_POINTS) - j_count
|
|
pts = J_POINTS[start:]
|
|
for i in range(len(pts) - 1):
|
|
draw.line([pts[i], pts[i + 1]], fill='black', width=2)
|
|
|
|
return im
|
|
|
|
|
|
P_POINTS = LoadingView._sample_path(_P_SEGMENTS, LoadingView.SCALE, LoadingView.dx, LoadingView.dy)
|
|
J_POINTS = LoadingView._sample_path(_J_SEGMENTS, LoadingView.SCALE, LoadingView.dx, LoadingView.dy)
|
|
|
|
FRAMES = LoadingView._build_frame_cache()
|