Skip to content

Control Screensaver Animation Speed Acroos Different Hardware & Emulator #733

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 2 commits into
base: dev
Choose a base branch
from
Open
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
119 changes: 81 additions & 38 deletions src/seedsigner/views/screensaver.py
Original file line number Diff line number Diff line change
Expand Up @@ -96,7 +96,7 @@ def _render(self):
# Display version num below SeedSigner logo
font = Fonts.get_font(GUIConstants.get_body_font_name(), GUIConstants.get_top_nav_title_font_size())
version = f"v{controller.VERSION}"

# The logo png is 240x240, but the actual logo is 70px tall, vertically centered

This comment was marked as resolved.

logo_height = 70
version_x = int(self.renderer.canvas_width/2)
Expand Down Expand Up @@ -150,28 +150,39 @@ def __init__(self, buttons):

self.min_coords = (0, 0)
self.max_coords = (self.logo.size[0], self.logo.size[1])

self.increment_x = self.rand_increment()
self.increment_y = self.rand_increment()

# Movement vector components - includes both direction and magnitude
self.vector_x = self.rand_direction()
self.vector_y = self.rand_direction()

self.cur_x = int(self.logo.size[0] / 2)
self.cur_y = int(self.logo.size[1] / 2)

self._is_running = False
self.last_screen = None

# Calculate min/max speed based on minimum screen dimension
min_screen_dimension = min(self.renderer.canvas_width, self.renderer.canvas_height)
# Min speed: cross the screen in 3 seconds, max: cross in 1 second
self.min_speed = min_screen_dimension / 3.0 # pixels per second
self.max_speed = min_screen_dimension / 1.0 # pixels per second
Copy link
Contributor

@kdmukai kdmukai Apr 21, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

In my testing, min / max speeds worked best if it took 1.5s - 10s to cross the screen in any one dimension.

But note that because of your rand_direction implementation, the speeds specified here do NOT correspond to the actual min/max speed you're achieving.

min_speed is X but you're scaling it by a fractional rand_direction so the actual minimum speed can be much slower, approaching zero.



@property
def is_running(self):
return self._is_running

def rand_speed(self):
"""Returns a random speed in pixels per second between min_speed and max_speed"""
return random.uniform(self.min_speed, self.max_speed)



def rand_direction(self):
"""Returns a random direction component between -1.0 and 1.0"""
return random.uniform(-1.0, 1.0)


def rand_increment(self):
max_increment = 10.0
min_increment = 1.0
increment = random.uniform(min_increment, max_increment)
if random.uniform(-1.0, 1.0) < 0.0:
return -1.0 * increment
return increment


def start(self):
Expand All @@ -183,47 +194,76 @@ def start(self):
# Store the current screen in order to restore it later
self.last_screen = self.renderer.canvas.copy()

screensaver_start = int(time.time() * 1000)
# Accumulate sub-pixel movements to avoid getting stuck at zero
accumulated_x = 0
accumulated_y = 0

# Screensaver must block any attempts to use the Renderer in another thread so it
# never gives up the lock until it returns.
with self.renderer.lock:
try:
last_update_time = time.time()
current_speed = self.rand_speed()

while self._is_running:
if self.buttons.has_any_input() or self.buttons.override_ind:
break

current_time = time.time()
elapsed_time = current_time - last_update_time

# Calculate pixel movement based on elapsed time and target speed
# Accumulate fractional movements to avoid getting stuck at zero
accumulated_x += elapsed_time * current_speed * self.vector_x
accumulated_y += elapsed_time * current_speed * self.vector_y

# Only move when we've accumulated at least 1 pixel of movement
pixels_to_move_x = int(accumulated_x)
pixels_to_move_y = int(accumulated_y)

# Remove the used whole pixels from accumulator
accumulated_x -= pixels_to_move_x
accumulated_y -= pixels_to_move_y

# Apply movement
if pixels_to_move_x != 0 or pixels_to_move_y != 0:
self.cur_x += pixels_to_move_x
self.cur_y += pixels_to_move_y

# Must crop the image to the exact display size
crop = self.image.crop((
self.cur_x, self.cur_y,
self.cur_x + self.renderer.canvas_width, self.cur_y + self.renderer.canvas_height))
self.renderer.disp.ShowImage(crop, 0, 0)

self.cur_x += self.increment_x
self.cur_y += self.increment_y

# At each edge bump, calculate a new random rate of change for that axis
if self.cur_x < self.min_coords[0]:
self.cur_x = self.min_coords[0]
self.increment_x = self.rand_increment()
if self.increment_x < 0.0:
self.increment_x *= -1.0
elif self.cur_x > self.max_coords[0]:
self.cur_x = self.max_coords[0]
self.increment_x = self.rand_increment()
if self.increment_x > 0.0:
self.increment_x *= -1.0

if self.cur_y < self.min_coords[1]:
self.cur_y = self.min_coords[1]
self.increment_y = self.rand_increment()
if self.increment_y < 0.0:
self.increment_y *= -1.0
elif self.cur_y > self.max_coords[1]:
self.cur_y = self.max_coords[1]
self.increment_y = self.rand_increment()
if self.increment_y > 0.0:
self.increment_y *= -1.0
# Handle boundary collisions and ensure we don't get stuck
if self.cur_x <= self.min_coords[0]:
self.cur_x = self.min_coords[0] + 1 # Move slightly away from edge
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

No need to worry about 1 pixel differences. The logo bounce is calculated from the center of the logo; there aren't any extreme edge scenarios to worry about.

self.vector_x = abs(self.rand_direction()) # Force positive
current_speed = self.rand_speed()
# Clear accumulator when changing direction
accumulated_x = 0
elif self.cur_x >= self.max_coords[0]:
self.cur_x = self.max_coords[0] - 1 # Move slightly away from edge
self.vector_x = -abs(self.rand_direction()) # Force negative
current_speed = self.rand_speed()
accumulated_x = 0

if self.cur_y <= self.min_coords[1]:
self.cur_y = self.min_coords[1] + 1 # Move slightly away from edge
self.vector_y = abs(self.rand_direction()) # Force positive
current_speed = self.rand_speed()
accumulated_y = 0
elif self.cur_y >= self.max_coords[1]:
self.cur_y = self.max_coords[1] - 1 # Move slightly away from edge
self.vector_y = -abs(self.rand_direction()) # Force negative
current_speed = self.rand_speed()
accumulated_y = 0

last_update_time = current_time

# Small sleep to prevent 100% CPU usage on faster hardware
time.sleep(0.001)

except KeyboardInterrupt as e:
# Exit triggered; close gracefully
Expand All @@ -239,8 +279,11 @@ def start(self):
self.renderer.show_image(self.last_screen)



def stop(self):
"""
Stops the screensaver animation cleanly.
"""
logger.info("Stopping screensaver")
self._is_running = False