1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 295 296 297 298 299 300 301 302 303 304 305 306 307
|
import math
from traits.api import Float, Property, List, Str, Range
from enable.api import Component
from kiva.trait_defs.kiva_font_trait import KivaFont
from kiva import affine
def percent_to_db(percent):
if percent == 0.0:
db = float('-inf')
else:
db = 20 * math.log10(percent / 100.0)
return db
def db_to_percent(db):
percent = math.pow(10, db / 20.0 + 2)
return percent
class VUMeter(Component):
# Value expressed in dB
db = Property(Float)
# Value expressed as a percent.
percent = Range(low=0.0)
# The maximum value to be display in the VU Meter, expressed as a percent.
max_percent = Float(150.0)
# Angle (in degrees) from a horizontal line through the hinge of the
# needle to the edge of the meter axis.
angle = Float(45.0)
# Values of the percentage-based ticks; these are drawn and labeled along
# the bottom of the curve axis.
percent_ticks = List(range(0, 101, 20))
# Text to write in the middle of the VU Meter.
text = Str("VU")
# Font used to draw `text`.
text_font = KivaFont("modern 48")
# Font for the db tick labels.
db_tick_font = KivaFont("modern 16")
# Font for the percent tick labels.
percent_tick_font = KivaFont("modern 12")
# beta is the fraction of the of needle that is "hidden".
# beta == 0 puts the hinge point of the needle on the bottom
# edge of the window. Values that result in a decent looking
# meter are 0 < beta < .65.
# XXX needs a better name!
_beta = Float(0.3)
# _outer_radial_margin is the radial extent beyond the circular axis
# to include in calculations of the space required for the meter.
# This allows room for the ticks and labels.
_outer_radial_margin = Float(60.0)
# The angle (in radians) of the span of the curve axis.
_phi = Property(Float, depends_on=['angle'])
# This is the radius of the circular axis (in screen coordinates).
_axis_radius = Property(Float, depends_on=['_phi', 'width', 'height'])
#---------------------------------------------------------------------
# Trait Property methods
#---------------------------------------------------------------------
def _get_db(self):
db = percent_to_db(self.percent)
return db
def _set_db(self, value):
self.percent = db_to_percent(value)
def _get__phi(self):
phi = math.pi * (180.0 - 2 * self.angle) / 180.0
return phi
def _get__axis_radius(self):
M = self._outer_radial_margin
beta = self._beta
w = self.width
h = self.height
phi = self._phi
R1 = w / (2 * math.sin(phi / 2)) - M
R2 = (h - M) / (1 - beta * math.cos(phi / 2))
R = min(R1, R2)
return R
#---------------------------------------------------------------------
# Trait change handlers
#---------------------------------------------------------------------
def _anytrait_changed(self):
self.request_redraw()
#---------------------------------------------------------------------
# Component API
#---------------------------------------------------------------------
def _draw_mainlayer(self, gc, view_bounds=None, mode="default"):
beta = self._beta
phi = self._phi
w = self.width
M = self._outer_radial_margin
R = self._axis_radius
# (ox, oy) is the position of the "hinge point" of the needle
# (i.e. the center of rotation). For beta > ~0, oy is negative,
# so this point is below the visible region.
ox = self.x + self.width // 2
oy = -beta * R * math.cos(phi / 2) + 1
left_theta = math.radians(180 - self.angle)
right_theta = math.radians(self.angle)
# The angle of the 100% position.
nominal_theta = self._percent_to_theta(100.0)
# The color of the axis for percent > 100.
red = (0.8, 0, 0)
with gc:
gc.set_antialias(True)
# Draw everything relative to the center of the circles.
gc.translate_ctm(ox, oy)
# Draw the primary ticks and tick labels on the curved axis.
gc.set_fill_color((0, 0, 0))
gc.set_font(self.db_tick_font)
for db in [-20, -10, -7, -5, -3, -2, -1, 0, 1, 2, 3]:
db_percent = db_to_percent(db)
theta = self._percent_to_theta(db_percent)
x1 = R * math.cos(theta)
y1 = R * math.sin(theta)
x2 = (R + 0.3 * M) * math.cos(theta)
y2 = (R + 0.3 * M) * math.sin(theta)
gc.set_line_width(2.5)
gc.move_to(x1, y1)
gc.line_to(x2, y2)
gc.stroke_path()
text = str(db)
if db > 0:
text = '+' + text
self._draw_rotated_label(gc, text, theta, R + 0.4 * M)
# Draw the secondary ticks on the curve axis.
for db in [-15, -9, -8, -6, -4, -0.5, 0.5]:
##db_percent = 100 * math.pow(10.0, db / 20.0)
db_percent = db_to_percent(db)
theta = self._percent_to_theta(db_percent)
x1 = R * math.cos(theta)
y1 = R * math.sin(theta)
x2 = (R + 0.2 * M) * math.cos(theta)
y2 = (R + 0.2 * M) * math.sin(theta)
gc.set_line_width(1.0)
gc.move_to(x1, y1)
gc.line_to(x2, y2)
gc.stroke_path()
# Draw the percent ticks and label on the bottom of the
# curved axis.
gc.set_font(self.percent_tick_font)
gc.set_fill_color((0.5, 0.5, 0.5))
gc.set_stroke_color((0.5, 0.5, 0.5))
percents = self.percent_ticks
for tick_percent in percents:
theta = self._percent_to_theta(tick_percent)
x1 = (R - 0.15 * M) * math.cos(theta)
y1 = (R - 0.15 * M) * math.sin(theta)
x2 = R * math.cos(theta)
y2 = R * math.sin(theta)
gc.set_line_width(2.0)
gc.move_to(x1, y1)
gc.line_to(x2, y2)
gc.stroke_path()
text = str(tick_percent)
if tick_percent == percents[-1]:
text = text + "%"
self._draw_rotated_label(gc, text, theta, R - 0.3 * M)
if self.text:
gc.set_font(self.text_font)
tx, ty, tw, th = gc.get_text_extent(self.text)
gc.set_fill_color((0, 0, 0, 0.25))
gc.set_text_matrix(affine.affine_from_rotation(0))
gc.set_text_position(-0.5 * tw,
(0.75 * beta + 0.25) * R)
gc.show_text(self.text)
# Draw the red curved axis.
gc.set_stroke_color(red)
w = 10
gc.set_line_width(w)
gc.arc(0, 0, R + 0.5 * w - 1, right_theta, nominal_theta)
gc.stroke_path()
# Draw the black curved axis.
w = 4
gc.set_line_width(w)
gc.set_stroke_color((0, 0, 0))
gc.arc(0, 0, R + 0.5 * w - 1, nominal_theta, left_theta)
gc.stroke_path()
# Draw the filled arc at the bottom.
gc.set_line_width(2)
gc.set_stroke_color((0, 0, 0))
gc.arc(0, 0, beta * R, math.radians(self.angle),
math.radians(180 - self.angle))
gc.stroke_path()
gc.set_fill_color((0, 0, 0, 0.25))
gc.arc(0, 0, beta * R, math.radians(self.angle),
math.radians(180 - self.angle))
gc.fill_path()
# Draw the needle.
percent = self.percent
# If percent exceeds max_percent, the needle is drawn at max_percent.
if percent > self.max_percent:
percent = self.max_percent
needle_theta = self._percent_to_theta(percent)
gc.rotate_ctm(needle_theta - 0.5 * math.pi)
self._draw_vertical_needle(gc)
#---------------------------------------------------------------------
# Private methods
#---------------------------------------------------------------------
def _draw_vertical_needle(self, gc):
""" Draw the needle of the meter, pointing straight up. """
beta = self._beta
R = self._axis_radius
end_y = beta * R
blob_y = R - 0.6 * self._outer_radial_margin
tip_y = R + 0.2 * self._outer_radial_margin
lw = 5
with gc:
gc.set_alpha(1)
gc.set_fill_color((0, 0, 0))
# Draw the needle from the bottom to the blob.
gc.set_line_width(lw)
gc.move_to(0, end_y)
gc.line_to(0, blob_y)
gc.stroke_path()
# Draw the thin part of the needle from the blob to the tip.
gc.move_to(lw, blob_y)
control_y = blob_y + 0.25 * (tip_y - blob_y)
gc.quad_curve_to( 0.2 * lw, control_y, 0, tip_y)
gc.quad_curve_to(-0.2 * lw, control_y, -lw, blob_y)
gc.line_to(lw, blob_y)
gc.fill_path()
# Draw the blob on the needle.
gc.arc(0, blob_y, 6.0, 0, 2 * math.pi)
gc.fill_path()
def _draw_rotated_label(self, gc, text, theta, radius):
tx, ty, tw, th = gc.get_text_extent(text)
rr = math.sqrt(radius ** 2 + (0.5 * tw) ** 2)
dtheta = math.atan2(0.5 * tw, radius)
text_theta = theta + dtheta
x = rr * math.cos(text_theta)
y = rr * math.sin(text_theta)
rot_theta = theta - 0.5 * math.pi
with gc:
gc.set_text_matrix(affine.affine_from_rotation(rot_theta))
gc.set_text_position(x, y)
gc.show_text(text)
def _percent_to_theta(self, percent):
""" Convert percent to the angle theta, in radians.
theta is the angle of the needle measured counterclockwise from
the horizontal (i.e. the traditional angle of polar coordinates).
"""
angle = (self.angle + (180.0 - 2 * self.angle) *
(self.max_percent - percent) / self.max_percent)
theta = math.radians(angle)
return theta
def _db_to_theta(self, db):
""" Convert db to the angle theta, in radians. """
percent = db_to_percent(db)
theta = self._percent_to_theta(percent)
return theta
|