Source code for enable.gadgets.vu_meter

# (C) Copyright 2005-2022 Enthought, Inc., Austin, TX
# All rights reserved.
#
# This software is provided without warranty under the terms of the BSD
# license included in LICENSE.txt and may be redistributed only under
# the conditions described in the aforementioned license. The license
# is also available online at http://www.enthought.com/licenses/BSD.txt
#
# Thanks for using Enthought open source!
import math

from traits.api import Float, Property, List, Str, Range
from enable.api import Component
from enable.trait_defs.kiva_font_trait import KivaFont
from kiva import affine


[docs]def percent_to_db(percent): if percent == 0.0: db = float("-inf") else: db = 20 * math.log10(percent / 100.0) return db
[docs]def db_to_percent(db): percent = math.pow(10, db / 20.0 + 2) return percent
[docs]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(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, observe=["angle"]) # This is the radius of the circular axis (in screen coordinates). _axis_radius = Property(Float, observe=["_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