import math, random
from ... project import importer
from . import segments as _segments
ENVELOPE_PATH = 'bibliopixel.control.envelope.functions'
[docs]class Envelope:
def __init__(self, enabled=True, base_value=0, period=None, phase=0,
offset=0, scale=1, loops=0, symmetry=0.5, reverse=False,
serpentine=False, power=1, frequency=None, duty_cycle=0.5):
self.enabled = enabled
self.base_value = base_value
self.phase = phase
self.offset = offset
self.scale = scale
self.loops = loops
self.symmetry = symmetry
self.duty_cycle = duty_cycle
self.reverse = reverse
self.serpentine = serpentine
self.power = power
if period:
self.period = period
elif frequency:
self.frequency = frequency
else:
self.period = 1
@property
def frequency(self):
return 1 / self.period
@frequency.setter
def frequency(self, f):
self.period = 1 / f
def _call(self, x):
raise NotImplementedError
def __call__(self, delta_time):
if self.enabled:
loop = delta_time / self.period
if not (self.loops and loop >= self.loops):
x = ((delta_time + self.phase) / self.period) % 1
if self.reverse:
x = 1 - x
if self.serpentine and loop % 2:
x = 1 - x
if self.power != 1:
x = x ** self.power
x = _apply_skew(x, 1 - self.duty_cycle)
x = self._call(x)
x = _apply_skew(x, self.symmetry)
return x * self.scale + self.offset
return self.base_value
[docs]class Linear(Envelope):
def _call(self, x):
return x
[docs]class Sine(Envelope):
def _call(self, x):
return math.sin(x * 2 * math.pi)
[docs]class Triangular(Envelope):
def _call(self, x):
return 2 * x if x < 0.5 else 2 - 2 * x
[docs]class Square(Envelope):
def _call(self, x):
return 0 if x < 0.5 else 1
[docs]class Random(Envelope):
def _call(self, x):
return random.random()
[docs]class Gaussian(Envelope):
def __init__(self, mean=0.5, stdev=0.25, **kwds):
super().__init__(**kwds)
self.stdev = stdev
self.mean = mean
def _call(self, x):
return random.gauss(self.mean, self.stdev)
[docs]class Segments(Envelope):
def __init__(self, segments=(), period=None, **kwds):
"""
If period is None, then it is set to be the length of the
segments.
Each segment is either a single number, indicating a level, or a pair of
numbers, indicating a level and a time. Levels without a time are
assigned the average delay.
"""
self.segments = _segments.Segments(segments)
period = period or self.segments.total_time
super().__init__(period=period, **kwds)
def _call(self, x):
return self.segments(x * self.segments.total_time, self.base_value)
def _apply_skew(x, skew):
if x == skew:
return 0.5
elif x < skew:
return 0.5 * x / skew
else:
return 0.5 + 0.5 * (x - skew) / (1 - skew)