add the loading screen

This commit is contained in:
2026-03-12 23:52:07 +01:00
parent e80cea7bbf
commit bec76d6796
7 changed files with 218 additions and 5 deletions

View File

@@ -2,4 +2,5 @@ from .number import NumberView
from .circle import CircleView
from .timer import TimerView
from .text import TextView
from .loading import LoadingView
from .main_view import MainView

98
frontend/views/loading.py Normal file
View File

@@ -0,0 +1,98 @@
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

View File

@@ -6,7 +6,7 @@ from PIL import ImageChops
from ..config import DISPLAY_TYPES, DISPLAY_MODES
from .draw_utils import draw_clock, draw_bluetooth_icon
from . import NumberView, CircleView, TimerView, TextView
from . import NumberView, CircleView, TimerView, TextView, LoadingView
from .button_interface import ButtonInterface
from .buttons_manager import ButtonsManager
@@ -24,7 +24,7 @@ class MainView(tk.Frame, ButtonInterface):
bt_stop_pairing_command=None,
**kwargs):
super().__init__(parent, **kwargs)
self.curr_mode = DISPLAY_MODES.MAIN
self.curr_mode = DISPLAY_MODES.LOADING
self.views = []
self.timer_view = None # Timer view is always active
self.tare_command = tare_command
@@ -51,6 +51,7 @@ class MainView(tk.Frame, ButtonInterface):
self.text_view = TextView(self.actions, self.im_size, self.center)
self.buttons = ButtonsManager(self, self.im_size, self.center,
curr_view=self)
self.loading_view = LoadingView(self.im_size, self.center)
self.recipes_manager = RecipeManager()
self.current_steps = [None] * 3
self.tare_buffer = []
@@ -280,7 +281,13 @@ class MainView(tk.Frame, ButtonInterface):
def refresh(self, weight: float):
ims = []
if self.curr_mode == DISPLAY_MODES.MAIN or self.curr_mode == DISPLAY_MODES.DO_RECIPE:
if self.curr_mode == DISPLAY_MODES.LOADING:
frame, done = self.loading_view.get_frame()
ims.append(frame)
if done:
self.curr_mode = DISPLAY_MODES.MAIN
elif self.curr_mode == DISPLAY_MODES.MAIN or self.curr_mode == DISPLAY_MODES.DO_RECIPE:
# Always include timer and button view
if self.curr_mode == DISPLAY_MODES.MAIN or \
(self.current_steps[1] is not None and \