Skip to content

Commit 10c616e

Browse files
authored
Merge pull request #2984 from adafruit/mdr_terminal
adding severance terminal code
2 parents 06d7ef1 + 20ee088 commit 10c616e

File tree

7 files changed

+1812
-0
lines changed

7 files changed

+1812
-0
lines changed
Lines changed: 300 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,300 @@
1+
# SPDX-FileCopyrightText: 2025 Liz Clark for Adafruit Industries
2+
# SPDX-License-Identifier: MIT
3+
4+
import math
5+
import random
6+
import tkinter as tk
7+
import time
8+
from palette import Palette
9+
10+
class DataNumber:
11+
active_bin = None
12+
13+
@classmethod
14+
def reset_active_bin(cls):
15+
"""Reset the class-level active bin tracker"""
16+
cls.active_bin = None
17+
18+
def __init__(self, x: int, y: int, canvas: tk.Canvas, base_size: int = 35, palette=Palette):
19+
"""
20+
Initialize a data number for macrodata refinement
21+
"""
22+
self.num = random.randint(0, 9)
23+
self.home_x = x
24+
self.home_y = y
25+
self.x = x
26+
self.y = y
27+
self.mouse_offset_x = 0
28+
self.mouse_offset_y = 0
29+
self.palette = palette
30+
self.color = self.palette.FG
31+
self.alpha = 255
32+
self.base_size = base_size
33+
self.size = base_size
34+
self.refined = False
35+
self.bin_it = False
36+
self.bin = None
37+
self.bin_pause_time = 2
38+
self.bin_pause = self.bin_pause_time
39+
self.canvas = canvas
40+
self.text_id = self.canvas.create_text(
41+
self.x, self.y,
42+
text=str(self.num),
43+
font=('Courier', self.size),
44+
fill=self.color,
45+
anchor='center'
46+
)
47+
self.needs_refinement = False
48+
self.wiggle_offset_x = 0
49+
self.wiggle_offset_y = 0
50+
51+
def refine(self, bin_obj=None, bins_list=None):
52+
"""
53+
Mark this number for refinement and assign it to a bin.
54+
"""
55+
if bin_obj is not None:
56+
if bin_obj.is_full():
57+
return False
58+
target_bin = bin_obj
59+
elif bins_list is not None:
60+
target_bin = self.get_non_full_bin_for_position(bins_list)
61+
if target_bin is None:
62+
return False
63+
else:
64+
raise ValueError("Either bin_obj or bins_list must be provided")
65+
self.bin_it = True
66+
if DataNumber.active_bin is None:
67+
DataNumber.active_bin = target_bin
68+
self.bin = target_bin
69+
else:
70+
if DataNumber.active_bin.is_full():
71+
DataNumber.active_bin = target_bin
72+
self.bin = DataNumber.active_bin
73+
return True
74+
75+
def get_non_full_bin_for_position(self, bins_list):
76+
"""
77+
Determine which available bin should open based on the position of this number.
78+
"""
79+
non_full_bins = [bin_obj for bin_obj in bins_list if not bin_obj.is_full()]
80+
if not non_full_bins:
81+
return None
82+
screen_width = self.canvas.winfo_width()
83+
original_bin_index = self.get_bin_index_for_position(screen_width, len(bins_list))
84+
closest_bin = None
85+
min_distance = float('inf')
86+
for bin_obj in non_full_bins:
87+
distance = abs(bin_obj.i - original_bin_index)
88+
if distance < min_distance:
89+
min_distance = distance
90+
closest_bin = bin_obj
91+
return closest_bin
92+
93+
def get_bin_index_for_position(self, screen_width, num_bins):
94+
"""
95+
Get the bin index that corresponds to this number's position
96+
"""
97+
bin_width = screen_width / num_bins
98+
bin_index = int(self.x / bin_width)
99+
bin_index = max(0, min(bin_index, num_bins - 1))
100+
return bin_index
101+
102+
def go_bin(self):
103+
"""Move toward the bin for refinement"""
104+
if self.bin:
105+
self.bin.open()
106+
if self.bin_pause <= 0:
107+
dx = self.bin.x - self.x
108+
dy = self.bin.y - self.y
109+
distance = math.sqrt(dx*dx + dy*dy)
110+
if distance < 20:
111+
self.alpha = int(255 * (distance / 20))
112+
if distance < 3:
113+
self.wiggle_offset_x = 0
114+
self.wiggle_offset_y = 0
115+
self.mouse_offset_x = 0
116+
self.mouse_offset_y = 0
117+
self.bin.add_number()
118+
self.reset()
119+
return
120+
easing = max(0.03, min(0.1, 5.0 / distance))
121+
self.x += dx * easing
122+
self.y += dy * easing
123+
fade_start_distance = self.distance(self.home_x, self.home_y,
124+
self.bin.x, self.bin.y) * 0.4
125+
current_distance = self.distance(self.x, self.y, self.bin.x, self.bin.y)
126+
if distance >= 20:
127+
self.alpha = self.map_value(current_distance, fade_start_distance, 20, 255, 55)
128+
self.update_display()
129+
if hasattr(self.bin, 'level_elements'):
130+
for element_id in self.bin.level_elements.values():
131+
self.canvas.tag_raise(self.text_id, element_id)
132+
self.bin.last_refined_time = int(time.time() * 1000)
133+
else:
134+
self.bin_pause -= 1
135+
if self.bin_pause > 0:
136+
pulse_size = self.base_size * (1.0 + 0.5 *
137+
(1.0 - (self.bin_pause / self.bin_pause_time)))
138+
self.set_size(pulse_size)
139+
if hasattr(self.bin, 'level_elements'):
140+
for element_id in self.bin.level_elements.values():
141+
self.canvas.tag_raise(self.text_id, element_id)
142+
143+
def reset(self):
144+
"""Reset the number after being binned."""
145+
self.num = random.randint(0, 9)
146+
self.x = self.home_x
147+
self.y = self.home_y
148+
self.wiggle_offset_x = 0
149+
self.wiggle_offset_y = 0
150+
self.mouse_offset_x = 0
151+
self.mouse_offset_y = 0
152+
self.refined = False
153+
self.bin_it = False
154+
self.bin = None
155+
self.color = self.palette.FG
156+
self.alpha = 255
157+
self.bin_pause = self.bin_pause_time
158+
self.update_display()
159+
still_active = False
160+
if not still_active and DataNumber.active_bin is not None:
161+
DataNumber.active_bin = None
162+
163+
def go_home(self):
164+
"""Move the number back to its home position with easing."""
165+
self.x = self.lerp(self.x, self.home_x, 0.1)
166+
self.y = self.lerp(self.y, self.home_y, 0.1)
167+
self.size = self.lerp(self.size, self.base_size, 0.1)
168+
self.update_display()
169+
170+
def set_size(self, sz):
171+
"""Set the size of the number."""
172+
self.size = sz
173+
self.update_display()
174+
175+
def turn(self, new_color):
176+
"""Change the color of the number."""
177+
self.color = new_color
178+
self.update_display()
179+
180+
def inside(self, x1, y1, x2, y2):
181+
"""Check if this number is inside the given rectangle."""
182+
return (
183+
self.x > min(x1, x2) and
184+
self.x < max(x1, x2) and
185+
self.y > min(y1, y2) and
186+
self.y < max(y1, y2)
187+
)
188+
189+
def show(self):
190+
"""Update the display of this number."""
191+
self.update_display()
192+
193+
def update_display(self):
194+
"""Update the text display with current properties and improved alpha handling"""
195+
if self.bin_it:
196+
digit_size = self.lerp(self.size, self.size * 2.5,
197+
self.map_value(self.bin_pause, self.bin_pause_time, 0, 0, 1))
198+
else:
199+
digit_size = self.size
200+
font = ('Courier', int(digit_size))
201+
clamped_alpha = max(0, min(255, self.alpha))
202+
if clamped_alpha == 0:
203+
self.canvas.itemconfig(self.text_id, state='hidden')
204+
return
205+
else:
206+
self.canvas.itemconfig(self.text_id, state='normal')
207+
if clamped_alpha < 255:
208+
bg_color = self.palette.BG
209+
fg_color = self.color
210+
alpha_ratio = clamped_alpha / 255.0
211+
if alpha_ratio < 0.05:
212+
display_color = self.blend_colors(bg_color, fg_color, 0.05)
213+
else:
214+
display_color = self.blend_colors(bg_color, fg_color, alpha_ratio)
215+
else:
216+
display_color = self.color
217+
self.canvas.itemconfig(self.text_id,
218+
text=str(self.num),
219+
font=font,
220+
fill=display_color)
221+
if not hasattr(self, 'wiggle_offset_x'):
222+
self.wiggle_offset_x = 0
223+
if not hasattr(self, 'wiggle_offset_y'):
224+
self.wiggle_offset_y = 0
225+
if not hasattr(self, 'mouse_offset_x'):
226+
self.mouse_offset_x = 0
227+
if not hasattr(self, 'mouse_offset_y'):
228+
self.mouse_offset_y = 0
229+
smooth_wiggle_x = round(self.wiggle_offset_x * 10) / 10
230+
smooth_wiggle_y = round(self.wiggle_offset_y * 10) / 10
231+
smooth_mouse_x = round(self.mouse_offset_x * 10) / 10
232+
smooth_mouse_y = round(self.mouse_offset_y * 10) / 10
233+
display_x = self.x + smooth_wiggle_x + smooth_mouse_x
234+
display_y = self.y + smooth_wiggle_y + smooth_mouse_y
235+
self.canvas.coords(self.text_id, display_x, display_y)
236+
237+
def resize(self, new_x, new_y):
238+
"""Update the home position when the window is resized."""
239+
self.home_x = new_x
240+
self.home_y = new_y
241+
242+
def show_wiggle(self, proximity_factor=0):
243+
"""Make the number threatening"""
244+
if self.needs_refinement and not self.bin_it:
245+
original_x, original_y = self.x, self.y
246+
smooth_x = round(self.wiggle_offset_x * 10) / 10
247+
smooth_y = round(self.wiggle_offset_y * 10) / 10
248+
self.x += smooth_x
249+
self.y += smooth_y
250+
original_color = self.color
251+
base_pulse = 0.7
252+
wave1 = math.sin(time.time() * 0.9) * 0.15
253+
wave2 = math.sin(time.time() * 1.8) * 0.05
254+
highlight_intensity = base_pulse + wave1 + wave2 + (proximity_factor * 0.2)
255+
highlight_intensity = max(0.6, min(1.0, highlight_intensity))
256+
if highlight_intensity > 0.82:
257+
self.color = self.palette.SELECT
258+
else:
259+
blend_amount = (highlight_intensity - 0.6) / 0.22
260+
self.color = self.blend_colors(self.palette.FG, self.palette.SELECT, blend_amount)
261+
self.update_display()
262+
self.x, self.y = original_x, original_y
263+
self.color = original_color
264+
265+
@staticmethod
266+
def lerp(start, end, amt):
267+
"""Linear interpolation between start and end by amt."""
268+
return start + (end - start) * amt
269+
270+
@staticmethod
271+
def map_value(value, start1, stop1, start2, stop2):
272+
"""Re-maps a number from one range to another."""
273+
if stop1 == start1:
274+
return start2
275+
return start2 + (stop2 - start2) * ((value - start1) / (stop1 - start1))
276+
277+
@staticmethod
278+
def distance(x1, y1, x2, y2):
279+
"""Calculate distance between two points."""
280+
return math.sqrt((x2 - x1) ** 2 + (y2 - y1) ** 2)
281+
282+
@staticmethod
283+
def hex_to_rgb(hex_color):
284+
"""Convert hex color to RGB values."""
285+
# Strip the # if it exists
286+
hex_color = hex_color.lstrip('#')
287+
return tuple(int(hex_color[i:i+2], 16) for i in (0, 2, 4))
288+
289+
@staticmethod
290+
def rgb_to_hex(rgb):
291+
"""Convert RGB tuple to hex color."""
292+
return f"#{rgb[0]:02x}{rgb[1]:02x}{rgb[2]:02x}"
293+
294+
@staticmethod
295+
def blend_colors(color1, color2, ratio):
296+
"""Blend two colors based on ratio (0-1)."""
297+
c1 = DataNumber.hex_to_rgb(color1)
298+
c2 = DataNumber.hex_to_rgb(color2)
299+
blended = tuple(int(c1[i] + (c2[i] - c1[i]) * ratio) for i in range(3))
300+
return DataNumber.rgb_to_hex(blended)

0 commit comments

Comments
 (0)