micropython-nano-gui/extras/widgets/eclock.py

224 wiersze
7.4 KiB
Python

# eclock.py Unusual clock display for nanogui
# see micropython-epaper/epd-clock
# Released under the MIT License (MIT). See LICENSE.
# Copyright (c) 2023 Peter Hinch
from cmath import rect, phase
from math import sin, cos, pi
from array import array
from gui.core.nanogui import DObject, Writer
from gui.core.colors import *
# **** BEGIN DISPLAY CONSTANTS ****
THETA = pi/3 # Intersection of arc with unit circle
PHI = pi/12 # Arc is +-30 minute segment
# **** BEGIN DERIVED CONSTANTS ****
RADIUS = sin(THETA) / sin(PHI)
XLT = cos(THETA) - RADIUS * cos(PHI) # Convert arc relative to [0,0] relative
RV = pi / 360 # Interpolate arc to 1 minute
TV = RV / 5 # Small increment << I minute
# OR = cos(THETA) - RADIUS * cos(PHI) + 0j # Origin of arc
# **** BEGIN VECTOR CODE ****
# A vector is a line on the complex plane defined by a tuple of two complex
# numbers. Vectors presented for display lie in the unit circle.
def conj(n): # Complex conjugate
return n.real - n.imag * 1j
# Generate vectors comprising sectors of an arc. hrs defines location of arc,
# angle its length.
# 1 <= hrs <= 12 0 <= angle < 60 in normal use
# To print full arc angle == 60
def arc(hrs, angle=60, mul=1.0):
vs = rect(RADIUS * mul, PHI) # Coords relative to arc origin
ve = rect(RADIUS * mul, PHI)
pe = PHI - angle * RV + TV
rv = rect(1, -RV) # Rotation vector for 1 minute (about OR)
rot = rect(1, (3 - hrs) * pi / 6) # hrs rotation (about [0,0])
while phase(vs) > pe:
ve *= rv
# Translate to 0, 0
yield ((vs + XLT) * rot, (ve + XLT) * rot)
vs *= rv
def progress(hrs, angle, mul0, mul1):
vs = rect(RADIUS * mul0, PHI) # Coords relative to arc origin
pe = PHI - angle * RV + TV
rv = rect(1, -RV) # CW Rotation vector for 1 minute (about OR)
rot = rect(1, (3 - hrs) * pi / 6) # hrs rotation (about [0,0])
while phase(vs) > pe: # CW
# Translate to 0, 0
yield (vs + XLT) * rot
vs *= rv
yield (vs + XLT) * rot
pe = PHI
vs = rect(RADIUS * mul1, PHI - angle * RV)
rv = conj(rv) # Reverse direction of rotation
while phase(vs) < pe: # CCW
yield (vs + XLT) * rot
vs *= rv
yield (vs + XLT) * rot
# Hour ticks for main circle
def hticks(length):
segs = 12
phi = 2 * pi / segs
rv = rect(1, phi)
vs = 1 + 0j
ve = vs * (1 - length)
for _ in range(segs):
ve *= rv
vs *= rv
yield vs, ve
# Generate vectors for the minutes ticks
def ticks(hrs, length):
vs = rect(RADIUS, PHI) # Coords relative to arc origin
ve = rect(RADIUS - length, PHI) # Short tick
ve1 = rect(RADIUS - 1.5 * length, PHI) # Long tick
ve2 = rect(RADIUS - 2.0 * length, PHI) # Extra long tick
rv = rect(1, -5 * RV) # Rotation vector for 5 minutes (about OR)
rot = rect(1, (3 - hrs) * pi / 6) # hrs rotation (about [0,0])
for n in range(13):
# Translate to 0, 0
if n == 6: # Overdrawn by hour pointer: visually cleaner if we skip
yield
elif n % 3 == 0:
yield ((vs + XLT) * rot, (ve2 + XLT) * rot) # Extra Long
elif n % 2 == 0:
yield ((vs + XLT) * rot, (ve1 + XLT) * rot) # Long
else:
yield ((vs + XLT) * rot, (ve + XLT) * rot) # Short
vs *= rv
ve *= rv
ve1 *= rv
ve2 *= rv
# Generate vector for the hour line
def hour(hrs):
rot = rect(1, (3 - hrs) * pi / 6) # hrs rotation (about [0,0])
return -rot, rot
# Points for arrow head
def head(hrs):
ve = 1 + 0j
rot = rect(1, (3 - hrs) * pi / 6) # hrs rotation (about [0,0])
yield ve * rot
vs = 0.9 + 0.1j
yield vs * rot
vs = conj(vs)
yield vs * rot
def tail(hrs):
rot = rect(1, (3 - hrs) * pi / 6) # Rotation
xlt = (-1.05 + 0j) * rot # Translation
phi = pi / 3
r = 0.13
vs = rect(r, phi)
vr = rect(1.0, -phi/6) # 6 segments per arc
while phase(vs) > -phi:
yield vs * rot + xlt
vs *= vr
yield vs * rot + xlt
# Generate vector for inner legends
def inner(hrs):
phi = pi * 0.35 #.33
length = 0.75
vec = rect(length, phi)
rot = rect(1, (3 - hrs) * pi / 6) # hrs rotation (about [0,0])
yield vec * rot
yield conj(vec) * rot
# **** BEGIN POPULATE DISPLAY ****
# colors: hour ticks, arc, mins ticks, mins arc, pointer
class EClock(DObject):
def __init__(self, writer, row, col, height, fgcolor=None, bgcolor=None, bdcolor=RED, int_colors=None):
super().__init__(writer, row, col, height, height, fgcolor, bgcolor, bdcolor)
self.colors = (WHITE, WHITE, WHITE, WHITE, WHITE) if int_colors is None else int_colors
self.radius = height / 2
self.xlat = self.col + 1j * self.row # Translation vector
# Convert from maths coords to graphics (invert y)
# Shift so real and imag are positive (0 <= re <= 2, 0 <= im <= 2)
# Multiply by widget size scalar
# Shift by adding widget position vector
def scale(self, point):
return (conj(point) + 1 + 1j) * self.radius + self.xlat
# Draw a vector scaling it for display and converting to integer x, y
def draw_vec(self, vec, color):
vs = self.scale(vec[0])
ve = self.scale(vec[1])
self.device.line(round(vs.real), round(vs.imag), round(ve.real), round(ve.imag), color)
def draw_poly(self, points, color):
xp = array("h")
for p in points:
p = self.scale(p)
xp.append(round(p.real))
xp.append(round(p.imag))
self.device.poly(0, 0, xp, color, True)
def map_point(self, point):
point = self.scale(point)
return round(point.imag), round(point.real)
def value(self, t):
super().value(t)
self.show()
def show(self): # Called by an async task whenever minutes changes
super().show()
wri = self.writer
dev = self.device
c = self.colors
t = super().value()
mins = t[4]
angle = mins + 30 if mins < 30 else mins - 30
# mins angle
# 5 35
# 29 59
# 31 1
# 59 29
if mins < 30:
hrs = (t[3] % 12)
else:
hrs = (t[3] + 1) % 12
# 0 <= hrs <= 11 changes on half-hour
# Draw graphics.
rad = round(self.height / 2)
row = self.row + rad
col = self.col + rad
dev.ellipse(col, row, rad, rad, self.fgcolor)
for vec in hticks(0.05):
self.draw_vec(vec, c[0])
for vec in arc(hrs): # -30 to +30 arc
self.draw_vec(vec, c[1]) # Arc
for vec in ticks(hrs, 0.05): # Ticks
if vec is not None: # Not overdrawn by hour pointer
self.draw_vec(vec, c[2])
self.draw_poly(progress(hrs, angle, 1.0, 0.99), c[3]) # Elapsed minutes
self.draw_vec(hour(hrs), c[4]) # Chevron shaft
self.draw_poly(head(hrs), c[4]) # Chevron head
self.draw_poly(tail(hrs), c[4]) # Chevron tail
# Inner text
co = round(self.writer.stringlen("+30") / 2) # Row and col offsets to
ro = round(self.writer.height / 2) # position text relative to string centre.
txt = "-30"
for point in inner(hrs):
row, col = self.map_point(point) # Convert to display coords
Writer.set_textpos(dev, row - ro, col - co)
wri.setcolor(self.fgcolor, self.bgcolor)
wri.printstring(txt, False)
txt = "+30"
wri.setcolor() # Restore defaults