mirror of
https://github.com/trezor/trezor-firmware.git
synced 2024-11-16 04:29:08 +00:00
split ui code in modules
This commit is contained in:
parent
5968eb3f5f
commit
f3c9715ff3
@ -1,29 +1,33 @@
|
||||
from trezor import ui
|
||||
from trezor.ui import utils as ui_utils
|
||||
from trezor.ui.swipe import Swipe
|
||||
from trezor import loop
|
||||
|
||||
|
||||
def swipe_to_change_orientation():
|
||||
while True:
|
||||
degrees = yield from Swipe().wait()
|
||||
ui.display.orientation(degrees)
|
||||
|
||||
|
||||
def layout_homescreen():
|
||||
print("Homescreen layout!")
|
||||
|
||||
# ui.display.bar(0, 0, 240, 240, ui.WHITE)
|
||||
|
||||
f = open('apps/homescreen/trezor.toig', 'rb')
|
||||
|
||||
def func(foreground):
|
||||
f.seek(0)
|
||||
ui.display.icon(0, 0, f.read(), foreground, ui.BLACK)
|
||||
|
||||
animation = ui.animate_pulse(func, ui.WHITE, ui.GREY, speed=400000)
|
||||
ui.display.icon(0, 0, f.read(), foreground, ui_utils.BLACK)
|
||||
|
||||
orientation = swipe_to_change_orientation()
|
||||
animation = ui_utils.animate_pulse(func, ui_utils.WHITE, ui_utils.GREY, speed=400000)
|
||||
timeout = loop.Sleep(5000 * 1000)
|
||||
|
||||
yield loop.Wait([animation, timeout])
|
||||
print('back to layout')
|
||||
|
||||
# try:
|
||||
# print(animation.throw(StopIteration()))
|
||||
# except:
|
||||
# pass
|
||||
yield loop.Wait([
|
||||
orientation,
|
||||
animation,
|
||||
timeout
|
||||
])
|
||||
|
||||
from apps import playground
|
||||
return playground.layout_tap_to_confirm('1BitkeyP2nDd5oa64x7AjvBbbwST54W5Zmx2', 110.126967, 'BTC')
|
||||
|
@ -1,222 +1,10 @@
|
||||
import utime
|
||||
from trezor import loop
|
||||
from trezor import ui
|
||||
from trezor.ui.swipe import Swipe
|
||||
from trezor.ui.pin import PinDialog, PIN_CONFIRMED, PIN_CANCELLED
|
||||
from trezor.utils import unimport_func
|
||||
|
||||
|
||||
def in_area(pos, area):
|
||||
x, y = pos
|
||||
ax, ay, aw, ah = area
|
||||
return ax <= x <= ax + aw and ay <= y <= ay + ah
|
||||
|
||||
|
||||
DEFAULT_BUTTON = {
|
||||
'bg-color': ui.BLACK,
|
||||
'fg-color': ui.WHITE,
|
||||
'text-style': ui.NORMAL,
|
||||
'border-color': ui.blend(ui.BLACK, ui.WHITE, 0.1),
|
||||
}
|
||||
DEFAULT_BUTTON_ACTIVE = {
|
||||
'bg-color': ui.GREY,
|
||||
'fg-color': ui.BLACK,
|
||||
'text-style': ui.BOLD,
|
||||
'border-color': ui.GREY,
|
||||
}
|
||||
|
||||
CANCEL_BUTTON = {
|
||||
'bg-color': ui.blend(ui.BLACK, ui.RED, 0.3),
|
||||
'fg-color': ui.RED,
|
||||
'text-style': ui.NORMAL,
|
||||
'border-color': ui.blend(ui.BLACK, ui.RED, 0.6),
|
||||
}
|
||||
CANCEL_BUTTON_ACTIVE = {
|
||||
'bg-color': ui.RED,
|
||||
'fg-color': ui.WHITE,
|
||||
'text-style': ui.BOLD,
|
||||
'border-color': ui.RED,
|
||||
}
|
||||
|
||||
CONFIRM_BUTTON = {
|
||||
'bg-color': ui.blend(ui.BLACK, ui.GREEN, 0.3),
|
||||
'fg-color': ui.GREEN,
|
||||
'text-style': ui.NORMAL,
|
||||
'border-color': ui.blend(ui.BLACK, ui.GREEN, 0.6),
|
||||
}
|
||||
CONFIRM_BUTTON_ACTIVE = {
|
||||
'bg-color': ui.GREEN,
|
||||
'fg-color': ui.WHITE,
|
||||
'text-style': ui.BOLD,
|
||||
'border-color': ui.GREEN,
|
||||
}
|
||||
|
||||
|
||||
BTN_CLICKED = const(1)
|
||||
|
||||
BTN_STARTED = const(1)
|
||||
BTN_ACTIVE = const(2)
|
||||
BTN_DIRTY = const(4)
|
||||
|
||||
|
||||
class Button():
|
||||
|
||||
def __init__(self, area, text, normal_style=None, active_style=None):
|
||||
self.area = area
|
||||
self.text = text
|
||||
self.normal_style = normal_style or DEFAULT_BUTTON
|
||||
self.active_style = active_style or DEFAULT_BUTTON_ACTIVE
|
||||
self.state = BTN_DIRTY
|
||||
|
||||
def render(self):
|
||||
if not self.state & BTN_DIRTY:
|
||||
return
|
||||
state = self.state & ~BTN_DIRTY
|
||||
style = self.active_style if state & BTN_ACTIVE else self.normal_style
|
||||
ax, ay, aw, ah = self.area
|
||||
tx = ax + aw // 2
|
||||
ty = ay + ah // 2 + 8
|
||||
ui.display.bar(ax, ay, aw, ah, style['border-color'])
|
||||
ui.display.bar(ax + 1, ay + 1, aw - 2, ah - 2, style['bg-color'])
|
||||
ui.display.text_center(tx, ty, self.text,
|
||||
style['text-style'],
|
||||
style['fg-color'],
|
||||
style['bg-color'])
|
||||
self.state = state
|
||||
|
||||
def send(self, event, pos):
|
||||
if event is loop.TOUCH_START:
|
||||
if in_area(pos, self.area):
|
||||
self.state = BTN_STARTED | BTN_DIRTY | BTN_ACTIVE
|
||||
elif event is loop.TOUCH_MOVE and self.state & BTN_STARTED:
|
||||
if in_area(pos, self.area):
|
||||
if not self.state & BTN_ACTIVE:
|
||||
self.state = BTN_STARTED | BTN_DIRTY | BTN_ACTIVE
|
||||
else:
|
||||
if self.state & BTN_ACTIVE:
|
||||
self.state = BTN_STARTED | BTN_DIRTY
|
||||
elif event is loop.TOUCH_END and self.state & BTN_STARTED:
|
||||
self.state = BTN_DIRTY
|
||||
if in_area(pos, self.area):
|
||||
return BTN_CLICKED
|
||||
|
||||
|
||||
def digit_area(d):
|
||||
width = const(80)
|
||||
height = const(60)
|
||||
x = ((d - 1) % 3) * width
|
||||
y = ((d - 1) // 3) * height
|
||||
return (x, y, width, height)
|
||||
|
||||
|
||||
PIN_CONFIRMED = const(1)
|
||||
PIN_CANCELLED = const(2)
|
||||
|
||||
|
||||
class PinDialog():
|
||||
|
||||
def __init__(self, pin=''):
|
||||
self.pin = pin
|
||||
self.confirm_button = Button((0, 240 - 60, 120, 60), 'Confirm',
|
||||
normal_style=CONFIRM_BUTTON,
|
||||
active_style=CONFIRM_BUTTON_ACTIVE)
|
||||
self.cancel_button = Button((120, 240 - 60, 120, 60), 'Cancel',
|
||||
normal_style=CANCEL_BUTTON,
|
||||
active_style=CANCEL_BUTTON_ACTIVE)
|
||||
self.pin_buttons = [Button(digit_area(dig), str(dig))
|
||||
for dig in range(1, 10)]
|
||||
|
||||
def render(self):
|
||||
for btn in self.pin_buttons:
|
||||
btn.render()
|
||||
self.confirm_button.render()
|
||||
self.cancel_button.render()
|
||||
|
||||
def send(self, event, pos):
|
||||
for btn in self.pin_buttons:
|
||||
if btn.send(event, pos) is BTN_CLICKED:
|
||||
self.pin += btn.text
|
||||
if self.confirm_button.send(event, pos) is BTN_CLICKED:
|
||||
return PIN_CONFIRMED
|
||||
if self.cancel_button.send(event, pos) is BTN_CLICKED:
|
||||
return PIN_CANCELLED
|
||||
|
||||
def wait_for_result(self):
|
||||
while True:
|
||||
self.render()
|
||||
event, *pos = yield loop.Select(loop.TOUCH_START,
|
||||
loop.TOUCH_MOVE,
|
||||
loop.TOUCH_END)
|
||||
result = self.send(event, pos)
|
||||
if result is not None:
|
||||
return result
|
||||
|
||||
|
||||
SWIPE_DISTANCE_THRESHOLD = const(20) # Min pixels in the primary direction
|
||||
SWIPE_VELOCITY_THRESHOLD = const(200) # Min pixels/second
|
||||
SWIPE_RATIO_THRESHOLD = const(30) # Max ratio secondary to primary direction in %
|
||||
|
||||
SWIPE_UP = const(180)
|
||||
SWIPE_DOWN = const(0)
|
||||
SWIPE_LEFT = const(90)
|
||||
SWIPE_RIGHT = const(270)
|
||||
|
||||
|
||||
class Swipe():
|
||||
|
||||
def __init__(self, area=None):
|
||||
self.area = area or (0, 0, 240, 240)
|
||||
self.start_pos = None
|
||||
self.start_time = 0
|
||||
self.end_pos = None
|
||||
self.end_time = 0
|
||||
|
||||
def send(self, event, pos):
|
||||
|
||||
if event is loop.TOUCH_START and in_area(pos, self.area):
|
||||
self.start_time = utime.time()
|
||||
self.start_pos = pos
|
||||
|
||||
elif event is loop.TOUCH_END and self.start_pos is not None:
|
||||
self.end_time = utime.time()
|
||||
self.end_pos = pos
|
||||
td = self.end_time - self.start_time
|
||||
pdx = self.end_pos[0] - self.start_pos[0]
|
||||
pdy = self.end_pos[1] - self.start_pos[1]
|
||||
pdxa = abs(pdx)
|
||||
pdya = abs(pdy)
|
||||
if pdxa > pdya:
|
||||
# Horizontal direction
|
||||
velx = pdx / td
|
||||
velxa = abs(velx)
|
||||
ratio = int(pdya / pdxa * 100) if pdxa > 0 else 100
|
||||
print('velxa', velxa, 'pdxa', pdxa, 'ratio', ratio)
|
||||
if (velxa >= SWIPE_VELOCITY_THRESHOLD
|
||||
and pdxa >= SWIPE_DISTANCE_THRESHOLD
|
||||
and ratio <= SWIPE_RATIO_THRESHOLD):
|
||||
return SWIPE_RIGHT if pdx > 0 else SWIPE_LEFT
|
||||
else:
|
||||
# Vertical direction
|
||||
vely = pdy / td
|
||||
velya = abs(vely)
|
||||
ratio = int(pdxa / pdya * 100) if pdya > 0 else 100
|
||||
print('velya', velya, 'pdya', pdya, 'ratio', ratio)
|
||||
if (velya >= SWIPE_VELOCITY_THRESHOLD
|
||||
and pdya >= SWIPE_DISTANCE_THRESHOLD
|
||||
and ratio <= SWIPE_RATIO_THRESHOLD):
|
||||
return SWIPE_DOWN if pdy > 0 else SWIPE_UP
|
||||
# No swipe, reset the state
|
||||
self.start_pos = None
|
||||
self.start_time = 0
|
||||
self.end_pos = None
|
||||
self.end_time = 0
|
||||
|
||||
def wait(self):
|
||||
while True:
|
||||
event, *pos = yield loop.Select(loop.TOUCH_START, loop.TOUCH_END)
|
||||
result = self.send(event, pos)
|
||||
if result is not None:
|
||||
return result
|
||||
|
||||
|
||||
def layout_tap_to_confirm(address, amount, currency):
|
||||
|
||||
# ui.display.bar(0, 0, 240, 40, ui.GREEN)
|
||||
@ -239,22 +27,14 @@ def layout_tap_to_confirm(address, amount, currency):
|
||||
|
||||
# animation = ui.animate_pulse(func, ui.BLACK, ui.GREY, speed=200000)
|
||||
|
||||
# pin_dialog = PinDialog()
|
||||
# pin_result = yield from pin_dialog.wait_for_result()
|
||||
pin_dialog = PinDialog()
|
||||
pin_result = yield from pin_dialog.wait_for_result()
|
||||
|
||||
# if pin_result is PIN_CONFIRMED:
|
||||
# print('PIN confirmed:', pin_dialog.pin)
|
||||
if pin_result is PIN_CONFIRMED:
|
||||
print('PIN confirmed:', pin_dialog.pin)
|
||||
|
||||
# elif pin_result is PIN_CANCELLED:
|
||||
# print('PIN CANCELLED, go home')
|
||||
|
||||
degrees = 0
|
||||
|
||||
while True:
|
||||
ui.display.bar(0, 0, 240, 240, ui.BLACK)
|
||||
ui.display.text_center(120, 130, 'HELLO WORLD!', ui.NORMAL, ui.WHITE, ui.BLACK)
|
||||
degrees = yield from Swipe().wait()
|
||||
ui.display.orientation(degrees)
|
||||
elif pin_result is PIN_CANCELLED:
|
||||
print('PIN CANCELLED, go home')
|
||||
|
||||
|
||||
@unimport_func
|
||||
|
3
src/trezor/ui/__init__.py
Normal file
3
src/trezor/ui/__init__.py
Normal file
@ -0,0 +1,3 @@
|
||||
from TrezorUi import Display
|
||||
|
||||
display = Display()
|
94
src/trezor/ui/button.py
Normal file
94
src/trezor/ui/button.py
Normal file
@ -0,0 +1,94 @@
|
||||
from . import utils
|
||||
from . import display
|
||||
from .utils import in_area
|
||||
from trezor import loop
|
||||
|
||||
|
||||
DEFAULT_BUTTON = {
|
||||
'bg-color': utils.BLACK,
|
||||
'fg-color': utils.WHITE,
|
||||
'text-style': utils.NORMAL,
|
||||
'border-color': utils.blend(utils.BLACK, utils.WHITE, 0.1),
|
||||
}
|
||||
DEFAULT_BUTTON_ACTIVE = {
|
||||
'bg-color': utils.GREY,
|
||||
'fg-color': utils.BLACK,
|
||||
'text-style': utils.BOLD,
|
||||
'border-color': utils.GREY,
|
||||
}
|
||||
|
||||
CANCEL_BUTTON = {
|
||||
'bg-color': utils.blend(utils.BLACK, utils.RED, 0.3),
|
||||
'fg-color': utils.RED,
|
||||
'text-style': utils.NORMAL,
|
||||
'border-color': utils.blend(utils.BLACK, utils.RED, 0.6),
|
||||
}
|
||||
CANCEL_BUTTON_ACTIVE = {
|
||||
'bg-color': utils.RED,
|
||||
'fg-color': utils.WHITE,
|
||||
'text-style': utils.BOLD,
|
||||
'border-color': utils.RED,
|
||||
}
|
||||
|
||||
CONFIRM_BUTTON = {
|
||||
'bg-color': utils.blend(utils.BLACK, utils.GREEN, 0.3),
|
||||
'fg-color': utils.GREEN,
|
||||
'text-style': utils.NORMAL,
|
||||
'border-color': utils.blend(utils.BLACK, utils.GREEN, 0.6),
|
||||
}
|
||||
CONFIRM_BUTTON_ACTIVE = {
|
||||
'bg-color': utils.GREEN,
|
||||
'fg-color': utils.WHITE,
|
||||
'text-style': utils.BOLD,
|
||||
'border-color': utils.GREEN,
|
||||
}
|
||||
|
||||
BTN_CLICKED = const(1)
|
||||
|
||||
BTN_STARTED = const(1)
|
||||
BTN_ACTIVE = const(2)
|
||||
BTN_DIRTY = const(4)
|
||||
|
||||
|
||||
class Button():
|
||||
|
||||
|
||||
|
||||
def __init__(self, area, text, normal_style=None, active_style=None):
|
||||
self.area = area
|
||||
self.text = text
|
||||
self.normal_style = normal_style or DEFAULT_BUTTON
|
||||
self.active_style = active_style or DEFAULT_BUTTON_ACTIVE
|
||||
self.state = BTN_DIRTY
|
||||
|
||||
def render(self):
|
||||
if not self.state & BTN_DIRTY:
|
||||
return
|
||||
state = self.state & ~BTN_DIRTY
|
||||
style = self.active_style if state & BTN_ACTIVE else self.normal_style
|
||||
ax, ay, aw, ah = self.area
|
||||
tx = ax + aw // 2
|
||||
ty = ay + ah // 2 + 8
|
||||
display.bar(ax, ay, aw, ah, style['border-color'])
|
||||
display.bar(ax + 1, ay + 1, aw - 2, ah - 2, style['bg-color'])
|
||||
display.text_center(tx, ty, self.text,
|
||||
style['text-style'],
|
||||
style['fg-color'],
|
||||
style['bg-color'])
|
||||
self.state = state
|
||||
|
||||
def send(self, event, pos):
|
||||
if event is loop.TOUCH_START:
|
||||
if in_area(pos, self.area):
|
||||
self.state = BTN_STARTED | BTN_DIRTY | BTN_ACTIVE
|
||||
elif event is loop.TOUCH_MOVE and self.state & BTN_STARTED:
|
||||
if in_area(pos, self.area):
|
||||
if not self.state & BTN_ACTIVE:
|
||||
self.state = BTN_STARTED | BTN_DIRTY | BTN_ACTIVE
|
||||
else:
|
||||
if self.state & BTN_ACTIVE:
|
||||
self.state = BTN_STARTED | BTN_DIRTY
|
||||
elif event is loop.TOUCH_END and self.state & BTN_STARTED:
|
||||
self.state = BTN_DIRTY
|
||||
if in_area(pos, self.area):
|
||||
return BTN_CLICKED
|
53
src/trezor/ui/pin.py
Normal file
53
src/trezor/ui/pin.py
Normal file
@ -0,0 +1,53 @@
|
||||
from . import button
|
||||
from trezor import loop
|
||||
|
||||
|
||||
def digit_area(d):
|
||||
width = const(80)
|
||||
height = const(60)
|
||||
x = ((d - 1) % 3) * width
|
||||
y = ((d - 1) // 3) * height
|
||||
return (x, y, width, height)
|
||||
|
||||
|
||||
PIN_CONFIRMED = const(1)
|
||||
PIN_CANCELLED = const(2)
|
||||
|
||||
|
||||
class PinDialog():
|
||||
|
||||
def __init__(self, pin=''):
|
||||
self.pin = pin
|
||||
self.confirm_button = button.Button((0, 240 - 60, 120, 60), 'Confirm',
|
||||
normal_style=button.CONFIRM_BUTTON,
|
||||
active_style=button.CONFIRM_BUTTON_ACTIVE)
|
||||
self.cancel_button = button.Button((120, 240 - 60, 120, 60), 'Cancel',
|
||||
normal_style=button.CANCEL_BUTTON,
|
||||
active_style=button.CANCEL_BUTTON_ACTIVE)
|
||||
self.pin_buttons = [button.Button(digit_area(dig), str(dig))
|
||||
for dig in range(1, 10)]
|
||||
|
||||
def render(self):
|
||||
for btn in self.pin_buttons:
|
||||
btn.render()
|
||||
self.confirm_button.render()
|
||||
self.cancel_button.render()
|
||||
|
||||
def send(self, event, pos):
|
||||
for btn in self.pin_buttons:
|
||||
if btn.send(event, pos) is button.BTN_CLICKED:
|
||||
self.pin += btn.text
|
||||
if self.confirm_button.send(event, pos) is button.BTN_CLICKED:
|
||||
return PIN_CONFIRMED
|
||||
if self.cancel_button.send(event, pos) is button.BTN_CLICKED:
|
||||
return PIN_CANCELLED
|
||||
|
||||
def wait_for_result(self):
|
||||
while True:
|
||||
self.render()
|
||||
event, *pos = yield loop.Select(loop.TOUCH_START,
|
||||
loop.TOUCH_MOVE,
|
||||
loop.TOUCH_END)
|
||||
result = self.send(event, pos)
|
||||
if result is not None:
|
||||
return result
|
67
src/trezor/ui/swipe.py
Normal file
67
src/trezor/ui/swipe.py
Normal file
@ -0,0 +1,67 @@
|
||||
import utime
|
||||
|
||||
from .utils import in_area
|
||||
from trezor import loop
|
||||
|
||||
|
||||
SWIPE_DISTANCE_THRESHOLD = const(20) # Min pixels in the primary direction
|
||||
SWIPE_VELOCITY_THRESHOLD = const(200) # Min pixels/second
|
||||
SWIPE_RATIO_THRESHOLD = const(30) # Max ratio secondary to primary direction in %
|
||||
|
||||
SWIPE_UP = const(180)
|
||||
SWIPE_DOWN = const(0)
|
||||
SWIPE_LEFT = const(90)
|
||||
SWIPE_RIGHT = const(270)
|
||||
|
||||
|
||||
class Swipe():
|
||||
|
||||
def __init__(self, area=None):
|
||||
self.area = area or (0, 0, 240, 240)
|
||||
self.start_pos = None
|
||||
self.start_time = 0
|
||||
self.end_pos = None
|
||||
self.end_time = 0
|
||||
|
||||
def send(self, event, pos):
|
||||
|
||||
if event is loop.TOUCH_START and in_area(pos, self.area):
|
||||
self.start_time = utime.time()
|
||||
self.start_pos = pos
|
||||
|
||||
elif event is loop.TOUCH_END and self.start_pos is not None:
|
||||
self.end_time = utime.time()
|
||||
self.end_pos = pos
|
||||
td = self.end_time - self.start_time
|
||||
pdx = self.end_pos[0] - self.start_pos[0]
|
||||
pdy = self.end_pos[1] - self.start_pos[1]
|
||||
pdxa = abs(pdx)
|
||||
pdya = abs(pdy)
|
||||
if pdxa > pdya:
|
||||
# Horizontal direction
|
||||
velxa = abs(pdx / td)
|
||||
ratio = int(pdya / pdxa * 100) if pdxa > 0 else 100
|
||||
if (velxa >= SWIPE_VELOCITY_THRESHOLD
|
||||
and pdxa >= SWIPE_DISTANCE_THRESHOLD
|
||||
and ratio <= SWIPE_RATIO_THRESHOLD):
|
||||
return SWIPE_RIGHT if pdx > 0 else SWIPE_LEFT
|
||||
else:
|
||||
# Vertical direction
|
||||
velya = abs(pdy / td)
|
||||
ratio = int(pdxa / pdya * 100) if pdya > 0 else 100
|
||||
if (velya >= SWIPE_VELOCITY_THRESHOLD
|
||||
and pdya >= SWIPE_DISTANCE_THRESHOLD
|
||||
and ratio <= SWIPE_RATIO_THRESHOLD):
|
||||
return SWIPE_DOWN if pdy > 0 else SWIPE_UP
|
||||
# No swipe, reset the state
|
||||
self.start_pos = None
|
||||
self.start_time = 0
|
||||
self.end_pos = None
|
||||
self.end_time = 0
|
||||
|
||||
def wait(self):
|
||||
while True:
|
||||
event, *pos = yield loop.Select(loop.TOUCH_START, loop.TOUCH_END)
|
||||
result = self.send(event, pos)
|
||||
if result is not None:
|
||||
return result
|
@ -1,15 +1,13 @@
|
||||
import math
|
||||
import utime
|
||||
|
||||
from TrezorUi import Display
|
||||
from trezor import loop
|
||||
|
||||
from . import loop
|
||||
|
||||
display = Display()
|
||||
|
||||
def rgbcolor(r: int, g: int, b: int) -> int:
|
||||
return ((r & 0xF8) << 8) | ((g & 0xFC) << 3) | ((b & 0xF8) >> 3)
|
||||
|
||||
|
||||
RED = rgbcolor(0xF4, 0x43, 0x36)
|
||||
PINK = rgbcolor(0xE9, 0x1E, 0x63)
|
||||
PURPLE = rgbcolor(0x9C, 0x27, 0xB0)
|
||||
@ -36,6 +34,13 @@ MONO = const(0)
|
||||
NORMAL = const(1)
|
||||
BOLD = const(2)
|
||||
|
||||
|
||||
def in_area(pos, area):
|
||||
x, y = pos
|
||||
ax, ay, aw, ah = area
|
||||
return ax <= x <= ax + aw and ay <= y <= ay + ah
|
||||
|
||||
|
||||
def lerpi(a: int, b: int, t: float) -> int:
|
||||
return int(a + t * (b - a))
|
||||
|
Loading…
Reference in New Issue
Block a user