CreativePython 1.1.6__tar.gz → 1.2__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- {creativepython-1.1.6/src/CreativePython.egg-info → creativepython-1.2}/PKG-INFO +1 -1
- {creativepython-1.1.6 → creativepython-1.2}/pyproject.toml +2 -2
- {creativepython-1.1.6 → creativepython-1.2}/src/CreativePython/GuiRenderer.py +0 -5
- {creativepython-1.1.6 → creativepython-1.2}/src/CreativePython/RealtimeAudioPlayer.py +9 -8
- {creativepython-1.1.6 → creativepython-1.2/src/CreativePython.egg-info}/PKG-INFO +1 -1
- {creativepython-1.1.6 → creativepython-1.2}/src/CreativePython.egg-info/SOURCES.txt +3 -0
- {creativepython-1.1.6 → creativepython-1.2}/src/CreativePython.egg-info/top_level.txt +1 -0
- creativepython-1.2/src/animation.py +554 -0
- {creativepython-1.1.6 → creativepython-1.2}/src/gui.py +9 -124
- {creativepython-1.1.6 → creativepython-1.2}/src/music.py +72 -81
- creativepython-1.2/tests/test_crescendo.py +82 -0
- creativepython-1.2/tests/test_crescendo_import.py +114 -0
- {creativepython-1.1.6 → creativepython-1.2}/LICENSE +0 -0
- {creativepython-1.1.6 → creativepython-1.2}/LICENSE-PSF +0 -0
- {creativepython-1.1.6 → creativepython-1.2}/MANIFEST.in +0 -0
- {creativepython-1.1.6 → creativepython-1.2}/README.md +0 -0
- {creativepython-1.1.6 → creativepython-1.2}/setup.cfg +0 -0
- {creativepython-1.1.6 → creativepython-1.2}/src/CreativePython/GuiHandler.py +0 -0
- {creativepython-1.1.6 → creativepython-1.2}/src/CreativePython/__init__.py +0 -0
- {creativepython-1.1.6 → creativepython-1.2}/src/CreativePython/nevmuse/Java-Comparison-Tests/advMetricRunner.pythonSurvey.py +0 -0
- {creativepython-1.1.6 → creativepython-1.2}/src/CreativePython/nevmuse/Java-Comparison-Tests/compareMetrics_Java-Vs-Python.py +0 -0
- {creativepython-1.1.6 → creativepython-1.2}/src/CreativePython/nevmuse/RunMetrics.py +0 -0
- {creativepython-1.1.6 → creativepython-1.2}/src/CreativePython/nevmuse/Surveyor.py +0 -0
- {creativepython-1.1.6 → creativepython-1.2}/src/CreativePython/nevmuse/__init__.py +0 -0
- {creativepython-1.1.6 → creativepython-1.2}/src/CreativePython/nevmuse/data/Confidence.py +0 -0
- {creativepython-1.1.6 → creativepython-1.2}/src/CreativePython/nevmuse/data/Contig.py +0 -0
- {creativepython-1.1.6 → creativepython-1.2}/src/CreativePython/nevmuse/data/ExtendedNote.py +0 -0
- {creativepython-1.1.6 → creativepython-1.2}/src/CreativePython/nevmuse/data/Histogram.py +0 -0
- {creativepython-1.1.6 → creativepython-1.2}/src/CreativePython/nevmuse/data/Judgement.py +0 -0
- {creativepython-1.1.6 → creativepython-1.2}/src/CreativePython/nevmuse/data/Measurement.py +0 -0
- {creativepython-1.1.6 → creativepython-1.2}/src/CreativePython/nevmuse/data/PianoRoll.py +0 -0
- {creativepython-1.1.6 → creativepython-1.2}/src/CreativePython/nevmuse/data/PianoRollOld.py +0 -0
- {creativepython-1.1.6 → creativepython-1.2}/src/CreativePython/nevmuse/data/__init__.py +0 -0
- {creativepython-1.1.6 → creativepython-1.2}/src/CreativePython/nevmuse/data/test_ExtendedNote.py +0 -0
- {creativepython-1.1.6 → creativepython-1.2}/src/CreativePython/nevmuse/data/test_Histogram.py +0 -0
- {creativepython-1.1.6 → creativepython-1.2}/src/CreativePython/nevmuse/data/test_Measurement.py +0 -0
- {creativepython-1.1.6 → creativepython-1.2}/src/CreativePython/nevmuse/data/test_PianoRoll_assertions.py +0 -0
- {creativepython-1.1.6 → creativepython-1.2}/src/CreativePython/nevmuse/data/test_PianoRoll_integration.py +0 -0
- {creativepython-1.1.6 → creativepython-1.2}/src/CreativePython/nevmuse/data/test_PianoRoll_quantization.py +0 -0
- {creativepython-1.1.6 → creativepython-1.2}/src/CreativePython/nevmuse/data/test_PianoRoll_unit.py +0 -0
- {creativepython-1.1.6 → creativepython-1.2}/src/CreativePython/nevmuse/metrics/Metric.py +0 -0
- {creativepython-1.1.6 → creativepython-1.2}/src/CreativePython/nevmuse/metrics/ZipfMetrics.py +0 -0
- {creativepython-1.1.6 → creativepython-1.2}/src/CreativePython/nevmuse/metrics/__init__.py +0 -0
- {creativepython-1.1.6 → creativepython-1.2}/src/CreativePython/nevmuse/metrics/simple/ChordDensityMetric.py +0 -0
- {creativepython-1.1.6 → creativepython-1.2}/src/CreativePython/nevmuse/metrics/simple/ChordDistanceMetric.py +0 -0
- {creativepython-1.1.6 → creativepython-1.2}/src/CreativePython/nevmuse/metrics/simple/ChordMetric.py +0 -0
- {creativepython-1.1.6 → creativepython-1.2}/src/CreativePython/nevmuse/metrics/simple/ChordNormalizedMetric.py +0 -0
- {creativepython-1.1.6 → creativepython-1.2}/src/CreativePython/nevmuse/metrics/simple/ChromaticToneMetric.py +0 -0
- {creativepython-1.1.6 → creativepython-1.2}/src/CreativePython/nevmuse/metrics/simple/ContourBasslineDurationMetric.py +0 -0
- {creativepython-1.1.6 → creativepython-1.2}/src/CreativePython/nevmuse/metrics/simple/ContourBasslineDurationQuantizedMetric.py +0 -0
- {creativepython-1.1.6 → creativepython-1.2}/src/CreativePython/nevmuse/metrics/simple/ContourBasslinePitchMetric.py +0 -0
- {creativepython-1.1.6 → creativepython-1.2}/src/CreativePython/nevmuse/metrics/simple/ContourMelodyDurationMetric.py +0 -0
- {creativepython-1.1.6 → creativepython-1.2}/src/CreativePython/nevmuse/metrics/simple/ContourMelodyDurationQuantizedMetric.py +0 -0
- {creativepython-1.1.6 → creativepython-1.2}/src/CreativePython/nevmuse/metrics/simple/ContourMelodyPitchMetric.py +0 -0
- {creativepython-1.1.6 → creativepython-1.2}/src/CreativePython/nevmuse/metrics/simple/DurationBigramMetric.py +0 -0
- {creativepython-1.1.6 → creativepython-1.2}/src/CreativePython/nevmuse/metrics/simple/DurationDistanceMetric.py +0 -0
- {creativepython-1.1.6 → creativepython-1.2}/src/CreativePython/nevmuse/metrics/simple/DurationMetric.py +0 -0
- {creativepython-1.1.6 → creativepython-1.2}/src/CreativePython/nevmuse/metrics/simple/DurationQuantizedBigramMetric.py +0 -0
- {creativepython-1.1.6 → creativepython-1.2}/src/CreativePython/nevmuse/metrics/simple/DurationQuantizedDistanceMetric.py +0 -0
- {creativepython-1.1.6 → creativepython-1.2}/src/CreativePython/nevmuse/metrics/simple/DurationQuantizedMetric.py +0 -0
- {creativepython-1.1.6 → creativepython-1.2}/src/CreativePython/nevmuse/metrics/simple/HarmonicBigramMetric.py +0 -0
- {creativepython-1.1.6 → creativepython-1.2}/src/CreativePython/nevmuse/metrics/simple/HarmonicConsonanceMetric.py +0 -0
- {creativepython-1.1.6 → creativepython-1.2}/src/CreativePython/nevmuse/metrics/simple/HarmonicIntervalMetric.py +0 -0
- {creativepython-1.1.6 → creativepython-1.2}/src/CreativePython/nevmuse/metrics/simple/MelodicBigramMetric.py +0 -0
- {creativepython-1.1.6 → creativepython-1.2}/src/CreativePython/nevmuse/metrics/simple/MelodicConsonanceMetric.py +0 -0
- {creativepython-1.1.6 → creativepython-1.2}/src/CreativePython/nevmuse/metrics/simple/MelodicIntervalMetric.py +0 -0
- {creativepython-1.1.6 → creativepython-1.2}/src/CreativePython/nevmuse/metrics/simple/PitchDistanceMetric.py +0 -0
- {creativepython-1.1.6 → creativepython-1.2}/src/CreativePython/nevmuse/metrics/simple/PitchDurationMetric.py +0 -0
- {creativepython-1.1.6 → creativepython-1.2}/src/CreativePython/nevmuse/metrics/simple/PitchDurationQuantizedMetric.py +0 -0
- {creativepython-1.1.6 → creativepython-1.2}/src/CreativePython/nevmuse/metrics/simple/PitchMetric.py +0 -0
- {creativepython-1.1.6 → creativepython-1.2}/src/CreativePython/nevmuse/metrics/simple/RestMetric.py +0 -0
- {creativepython-1.1.6 → creativepython-1.2}/src/CreativePython/nevmuse/metrics/simple/__init__.py +0 -0
- {creativepython-1.1.6 → creativepython-1.2}/src/CreativePython/nevmuse/metrics/simple/test_DurationMetric.py +0 -0
- {creativepython-1.1.6 → creativepython-1.2}/src/CreativePython/nevmuse/metrics/simple/test_Metrics_BasicIntervalsAndBigrams.py +0 -0
- {creativepython-1.1.6 → creativepython-1.2}/src/CreativePython/nevmuse/metrics/simple/test_Metrics_ChordsAndConsonance.py +0 -0
- {creativepython-1.1.6 → creativepython-1.2}/src/CreativePython/nevmuse/metrics/simple/test_Metrics_ContoursAndChromatic.py +0 -0
- {creativepython-1.1.6 → creativepython-1.2}/src/CreativePython/nevmuse/metrics/simple/test_Metrics_QuantizedDurationsAndDistances.py +0 -0
- {creativepython-1.1.6 → creativepython-1.2}/src/CreativePython/nevmuse/metrics/simple/test_PitchMetric.py +0 -0
- {creativepython-1.1.6 → creativepython-1.2}/src/CreativePython/nevmuse/metrics/simple/test_RestMetric.py +0 -0
- {creativepython-1.1.6 → creativepython-1.2}/src/CreativePython/nevmuse/metrics/test_Metric.py +0 -0
- {creativepython-1.1.6 → creativepython-1.2}/src/CreativePython/nevmuse/utilities/CSVWriter.py +0 -0
- {creativepython-1.1.6 → creativepython-1.2}/src/CreativePython/nevmuse/utilities/PowerLawRandom.py +0 -0
- {creativepython-1.1.6 → creativepython-1.2}/src/CreativePython/nevmuse/utilities/__init__.py +0 -0
- {creativepython-1.1.6 → creativepython-1.2}/src/CreativePython/notationRenderer.py +0 -0
- {creativepython-1.1.6 → creativepython-1.2}/src/CreativePython.egg-info/dependency_links.txt +0 -0
- {creativepython-1.1.6 → creativepython-1.2}/src/CreativePython.egg-info/requires.txt +0 -0
- {creativepython-1.1.6 → creativepython-1.2}/src/bin/libportaudio.2.dylib +0 -0
- {creativepython-1.1.6 → creativepython-1.2}/src/iannix.py +0 -0
- {creativepython-1.1.6 → creativepython-1.2}/src/image.py +0 -0
- {creativepython-1.1.6 → creativepython-1.2}/src/markov.py +0 -0
- {creativepython-1.1.6 → creativepython-1.2}/src/midi.py +0 -0
- {creativepython-1.1.6 → creativepython-1.2}/src/osc.py +0 -0
- {creativepython-1.1.6 → creativepython-1.2}/src/timer.py +0 -0
- {creativepython-1.1.6 → creativepython-1.2}/src/zipf.py +0 -0
- {creativepython-1.1.6 → creativepython-1.2}/tests/testAnimate.py +0 -0
- {creativepython-1.1.6 → creativepython-1.2}/tests/testCompress.py +0 -0
- {creativepython-1.1.6 → creativepython-1.2}/tests/testGameboard.py +0 -0
- {creativepython-1.1.6 → creativepython-1.2}/tests/testHitTesting.py +0 -0
- {creativepython-1.1.6 → creativepython-1.2}/tests/testPeer.py +0 -0
- {creativepython-1.1.6 → creativepython-1.2}/tests/testToolTips.py +0 -0
- {creativepython-1.1.6 → creativepython-1.2}/tests/test_keyEvent.py +0 -0
- {creativepython-1.1.6 → creativepython-1.2}/tests/test_midi.py +0 -0
- {creativepython-1.1.6 → creativepython-1.2}/tests/test_osc.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: CreativePython
|
|
3
|
-
Version: 1.
|
|
3
|
+
Version: 1.2
|
|
4
4
|
Summary: A Python-based software environment for developing algorithmic art projects.
|
|
5
5
|
Author-email: "Dr. Bill Manaris" <manaris@cofc.edu>, Taj Ballinger <ballingertj@g.cofc.edu>, Trevor Ritchie <ritchiets@g.cofc.edu>
|
|
6
6
|
License: MIT License
|
|
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
|
|
|
4
4
|
|
|
5
5
|
[project]
|
|
6
6
|
name = "CreativePython"
|
|
7
|
-
version = "1.
|
|
7
|
+
version = "1.2"
|
|
8
8
|
description = "A Python-based software environment for developing algorithmic art projects."
|
|
9
9
|
readme = "README.md"
|
|
10
10
|
license = { file = "LICENSE" }
|
|
@@ -40,7 +40,7 @@ dependencies = [
|
|
|
40
40
|
|
|
41
41
|
[tool.setuptools]
|
|
42
42
|
package-dir = {"" = "src"}
|
|
43
|
-
py-modules = ["gui", "image", "midi", "music", "osc", "timer", "iannix", "markov", "zipf"]
|
|
43
|
+
py-modules = ["gui", "image", "midi", "music", "osc", "timer", "animation", "iannix", "markov", "zipf"]
|
|
44
44
|
|
|
45
45
|
[tool.setuptools.packages.find]
|
|
46
46
|
where = ["src"]
|
|
@@ -997,8 +997,6 @@ class DisplayMirror:
|
|
|
997
997
|
else:
|
|
998
998
|
item.qObject.setZValue(qZValue)
|
|
999
999
|
self._scene.addItem(item.qObject) # attach QGraphicsItem to scene
|
|
1000
|
-
cacheMode = QtWidgets.QGraphicsItem.CacheMode.DeviceCoordinateCache
|
|
1001
|
-
item.qObject.setCacheMode(cacheMode)
|
|
1002
1000
|
|
|
1003
1001
|
posArgs = {}
|
|
1004
1002
|
if x is not None:
|
|
@@ -2874,9 +2872,6 @@ class GroupMirror(_DrawableMirror):
|
|
|
2874
2872
|
item.qObject.setZValue(qZValue)
|
|
2875
2873
|
self.qObject.addToGroup(item.qObject)
|
|
2876
2874
|
|
|
2877
|
-
cacheMode = QtWidgets.QGraphicsItem.CacheMode.DeviceCoordinateCache
|
|
2878
|
-
item.qObject.setCacheMode(cacheMode)
|
|
2879
|
-
|
|
2880
2875
|
def _removeChild(self, args, responseId):
|
|
2881
2876
|
"""Removes a child from the group."""
|
|
2882
2877
|
itemId = args.get('itemId')
|
|
@@ -306,9 +306,9 @@ class _RealtimeAudioPlayer:
|
|
|
306
306
|
self.loopRegionStartFrame = max(0.0, float(loopRegionStartFrame))
|
|
307
307
|
self.loopRegionEndFrame = float(loopRegionEndFrame) if loopRegionEndFrame is not None else -1.0
|
|
308
308
|
|
|
309
|
-
# adjust the loop region end frame so it does not exceed the
|
|
309
|
+
# adjust the loop region end frame so it does not exceed the end of the audio file
|
|
310
310
|
if self.loopRegionEndFrame > 0:
|
|
311
|
-
self.loopRegionEndFrame = min(self.loopRegionEndFrame, self.numFrames
|
|
311
|
+
self.loopRegionEndFrame = min(self.loopRegionEndFrame, float(self.numFrames))
|
|
312
312
|
|
|
313
313
|
# ensure the loop region is valid: if the start frame is after or equal to the end frame, reset to default (full file)
|
|
314
314
|
if self.numFrames > 0 and self.loopRegionEndFrame > 0 and self.loopRegionStartFrame >= self.loopRegionEndFrame:
|
|
@@ -689,7 +689,8 @@ class _RealtimeAudioPlayer:
|
|
|
689
689
|
|
|
690
690
|
# determine where playback should end for this segment
|
|
691
691
|
# NOTE: supports three modes: natural EOF, loop region end, or play(size) duration
|
|
692
|
-
effectiveSegmentEndFrame
|
|
692
|
+
# effectiveSegmentEndFrame is the exclusive end (first frame NOT to play)
|
|
693
|
+
effectiveSegmentEndFrame = float(numAudioFrames)
|
|
693
694
|
if looping and self.loopRegionEndFrame > 0:
|
|
694
695
|
effectiveSegmentEndFrame = self.loopRegionEndFrame
|
|
695
696
|
elif not looping and self.targetEndSourceFrame > 0:
|
|
@@ -699,14 +700,13 @@ class _RealtimeAudioPlayer:
|
|
|
699
700
|
# NOTE: batch boundary detection avoids per-sample checks (major optimization)
|
|
700
701
|
framesToProcess = frames
|
|
701
702
|
willHitBoundary = False
|
|
702
|
-
boundaryFrame = -1
|
|
703
703
|
|
|
704
704
|
if effectiveSegmentEndFrame > 0:
|
|
705
705
|
framesToEndpoint = (effectiveSegmentEndFrame - self.playbackPosition) / rateFactor
|
|
706
|
-
if framesToEndpoint < frames
|
|
706
|
+
if 0 <= framesToEndpoint < frames:
|
|
707
707
|
willHitBoundary = True
|
|
708
|
-
|
|
709
|
-
framesToProcess = min(frames,
|
|
708
|
+
# ceil gives the number of frames that stay strictly within the endpoint
|
|
709
|
+
framesToProcess = min(frames, int(np.ceil(framesToEndpoint)))
|
|
710
710
|
|
|
711
711
|
###########################################################################
|
|
712
712
|
# PHASE 2: PITCH SHIFTING VIA TIME-STRETCH INTERPOLATION
|
|
@@ -856,7 +856,8 @@ class _RealtimeAudioPlayer:
|
|
|
856
856
|
outdata[framesToProcess:, :] = 0.0
|
|
857
857
|
|
|
858
858
|
# safety - fill any unfilled portion with silence
|
|
859
|
-
|
|
859
|
+
# skip when willHitBoundary is True: boundary handling already filled remainder (via recursion or explicit zero-fill)
|
|
860
|
+
if framesToProcess < frames and self.isPlaying and not willHitBoundary:
|
|
860
861
|
outdata[framesToProcess:, :] = 0.0
|
|
861
862
|
|
|
862
863
|
# update pan smoothing (prevents clicks from abrupt pan changes)
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: CreativePython
|
|
3
|
-
Version: 1.
|
|
3
|
+
Version: 1.2
|
|
4
4
|
Summary: A Python-based software environment for developing algorithmic art projects.
|
|
5
5
|
Author-email: "Dr. Bill Manaris" <manaris@cofc.edu>, Taj Ballinger <ballingertj@g.cofc.edu>, Trevor Ritchie <ritchiets@g.cofc.edu>
|
|
6
6
|
License: MIT License
|
|
@@ -3,6 +3,7 @@ LICENSE-PSF
|
|
|
3
3
|
MANIFEST.in
|
|
4
4
|
README.md
|
|
5
5
|
pyproject.toml
|
|
6
|
+
src/animation.py
|
|
6
7
|
src/gui.py
|
|
7
8
|
src/iannix.py
|
|
8
9
|
src/image.py
|
|
@@ -93,6 +94,8 @@ tests/testGameboard.py
|
|
|
93
94
|
tests/testHitTesting.py
|
|
94
95
|
tests/testPeer.py
|
|
95
96
|
tests/testToolTips.py
|
|
97
|
+
tests/test_crescendo.py
|
|
98
|
+
tests/test_crescendo_import.py
|
|
96
99
|
tests/test_keyEvent.py
|
|
97
100
|
tests/test_midi.py
|
|
98
101
|
tests/test_osc.py
|
|
@@ -0,0 +1,554 @@
|
|
|
1
|
+
#######################################################################################
|
|
2
|
+
# animation.py Version 1.0 18-Apr-2026 Bill Manaris and Taj Ballinger
|
|
3
|
+
#######################################################################################
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
# The animation library provides a simplified way to automate repeated calling of functions
|
|
7
|
+
# for animation or other automation effects. Examples may range from continuously animating
|
|
8
|
+
# GUI graphics (similar to MIT Processing's draw() loop), to sequencing precise, data-driven
|
|
9
|
+
# musical automation (such as volume envelopes or filter sweeps).
|
|
10
|
+
#
|
|
11
|
+
# It uses a single "master" timer, which makes sure that all visual, audio, and other effects
|
|
12
|
+
# are perfectly synchronized (unlike creating individual timers for each). This is very important.
|
|
13
|
+
#
|
|
14
|
+
# It provides an Animate static class that can register functions to call repeatedly to support various scenarios.
|
|
15
|
+
# Anything more involved can be created using regular timers (via the timer library).
|
|
16
|
+
#
|
|
17
|
+
# Additionally, it provides a few shorthand functions, like animate(), setAnimationRate(), etc.
|
|
18
|
+
#
|
|
19
|
+
# NOTE: Animation engine is initialized (i.e., master timer starts ticking) at module load time!!!
|
|
20
|
+
# This doesn't cost much, and simplifies life...
|
|
21
|
+
|
|
22
|
+
class Animate:
|
|
23
|
+
"""
|
|
24
|
+
Animate provides methods to automate calling of user-provided functions using a common, lock-step
|
|
25
|
+
time interval (or framerate).
|
|
26
|
+
"""
|
|
27
|
+
_ENGINE = None # master timer, created and started at module load time
|
|
28
|
+
_interval = 1000.0 / 60.0 # current timer interval (in ms)
|
|
29
|
+
_actionList = [] # list of registered callback functions to automate
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
@staticmethod
|
|
33
|
+
def _init():
|
|
34
|
+
"""
|
|
35
|
+
Initialize and start master animation timer.
|
|
36
|
+
"""
|
|
37
|
+
|
|
38
|
+
# initialize only if engine has not been created previously
|
|
39
|
+
if Animate._ENGINE is None: # no engine available?
|
|
40
|
+
|
|
41
|
+
# yes, so create a new engine and start it
|
|
42
|
+
from timer import Timer
|
|
43
|
+
|
|
44
|
+
# use a regular timer (i.e., our master timer) to call every interval
|
|
45
|
+
# our tick function (which in turns calls all registered functions appropriately)
|
|
46
|
+
# Timer(delay, callback, arguments, repeat)
|
|
47
|
+
Animate._ENGINE = Timer(Animate._interval, Animate._tick, [], True) # create it
|
|
48
|
+
Animate._ENGINE.start() # and start it!!!
|
|
49
|
+
|
|
50
|
+
# now, the master timer is running, ready to call any function that gets registered
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
@staticmethod
|
|
54
|
+
def _tick():
|
|
55
|
+
"""
|
|
56
|
+
Call each registered callback function once.
|
|
57
|
+
"""
|
|
58
|
+
# iterate through the list of registered actions
|
|
59
|
+
for action in Animate._actionList:
|
|
60
|
+
|
|
61
|
+
if callable(action): # is this a function?
|
|
62
|
+
|
|
63
|
+
# yes, so execute it!!
|
|
64
|
+
action()
|
|
65
|
+
|
|
66
|
+
# now, all registered functions have been called one time
|
|
67
|
+
|
|
68
|
+
|
|
69
|
+
@staticmethod
|
|
70
|
+
def resume():
|
|
71
|
+
"""
|
|
72
|
+
Resume the animation.
|
|
73
|
+
"""
|
|
74
|
+
|
|
75
|
+
if Animate._ENGINE is not None: # do we have an engine?
|
|
76
|
+
|
|
77
|
+
# yes, so resume it (starting it continues where we left off...)
|
|
78
|
+
Animate._ENGINE.start()
|
|
79
|
+
|
|
80
|
+
|
|
81
|
+
@staticmethod
|
|
82
|
+
def pause():
|
|
83
|
+
"""
|
|
84
|
+
Pause the animation.
|
|
85
|
+
"""
|
|
86
|
+
|
|
87
|
+
if Animate._ENGINE is not None: # do we have an engine?
|
|
88
|
+
|
|
89
|
+
# yes, so pause it (stopping simply stops ticking - everything else remains in place!!)
|
|
90
|
+
Animate._ENGINE.stop()
|
|
91
|
+
|
|
92
|
+
|
|
93
|
+
@staticmethod
|
|
94
|
+
def add(action):
|
|
95
|
+
"""
|
|
96
|
+
Register action (a callback function) to automate calling it.
|
|
97
|
+
A function may be registered several times (hopefully with different parameters).
|
|
98
|
+
"""
|
|
99
|
+
|
|
100
|
+
if callable(action): # is this a function?
|
|
101
|
+
|
|
102
|
+
# yes, so add it to list of functions to be called
|
|
103
|
+
Animate._actionList.append(action)
|
|
104
|
+
|
|
105
|
+
else:
|
|
106
|
+
|
|
107
|
+
# let them know that something is wrong...
|
|
108
|
+
print(f"Animate.add(): '{action}' is not a callable function.")
|
|
109
|
+
|
|
110
|
+
|
|
111
|
+
@staticmethod
|
|
112
|
+
def addWithValues(action, values, duration=None, repeat=1, whenDone=None):
|
|
113
|
+
"""
|
|
114
|
+
Register 'action' (a callback function) to be called repeatedly, passing it a sequence
|
|
115
|
+
of 'values' over a specified 'duration'. This will be repeated 'repeat' times.
|
|
116
|
+
When this process is finished, function 'whenDone' is called (if provided).
|
|
117
|
+
"""
|
|
118
|
+
|
|
119
|
+
if not callable(action): # is it NOT a function?
|
|
120
|
+
|
|
121
|
+
# let them know something is wrong...
|
|
122
|
+
print(f"Animate.addWithValues(): '{action}' is not a callable function.")
|
|
123
|
+
|
|
124
|
+
elif len(values) == 0: # is the list empty?
|
|
125
|
+
|
|
126
|
+
# let them know something is wrong...
|
|
127
|
+
print("Animate.addWithValues(): 'values' list is empty.")
|
|
128
|
+
|
|
129
|
+
else: # value list is not empty, so...
|
|
130
|
+
|
|
131
|
+
if duration is None: # default duration?
|
|
132
|
+
|
|
133
|
+
timeDelayBetweenValues = Animate._interval # use engine's default interval
|
|
134
|
+
|
|
135
|
+
else: # they have provided their own time-stretching, so...
|
|
136
|
+
|
|
137
|
+
# calculate time delay (in millisecs) between values
|
|
138
|
+
timeDelayBetweenValues = (duration * 1000.0) / len(values)
|
|
139
|
+
|
|
140
|
+
# now, timeDelayBetweenValues contains how long we should wait to process next value...
|
|
141
|
+
|
|
142
|
+
# check if this makes sense...
|
|
143
|
+
if timeDelayBetweenValues < Animate._interval: # is duration provided too small for our frame rate?
|
|
144
|
+
|
|
145
|
+
# calculate the minimum possible duration for the error message
|
|
146
|
+
minAllowableDuration = (Animate._interval * len(values)) / 1000.0
|
|
147
|
+
|
|
148
|
+
# let them know something is wrong...
|
|
149
|
+
print(f"Animate.addWithValues(): duration ({duration}s) is too short to process, given frame rate ({Animate.getRate()})")
|
|
150
|
+
print(f" Should be at least {minAllowableDuration:.2f} secs.")
|
|
151
|
+
|
|
152
|
+
else: # all good, so proceed...
|
|
153
|
+
|
|
154
|
+
import time
|
|
155
|
+
|
|
156
|
+
#####
|
|
157
|
+
# Now, we will build a closure (inner function) to handle processing each individual value,
|
|
158
|
+
# as well as anything else that may need to happen (i.e., start over for the next repetition,
|
|
159
|
+
# or call the 'whenDone' function if we are completely done - after the last value).
|
|
160
|
+
|
|
161
|
+
# A closure needs some non-local (i.e., external to it) variables to remember how far it has gone.
|
|
162
|
+
|
|
163
|
+
# NOTE: Every time addWithValues() above gets called, new variables are created here,
|
|
164
|
+
# and these are combined with the closure below, maintaining its state persistently
|
|
165
|
+
# without overwriting each other - it works well.
|
|
166
|
+
|
|
167
|
+
# external variables to be wrapped within the closure
|
|
168
|
+
valuesIndex = 0 # points to next value to be processed
|
|
169
|
+
repeatCounter = 0 # how many repetitions done so far
|
|
170
|
+
|
|
171
|
+
# timer delay (in millisecs) for each value
|
|
172
|
+
delay = timeDelayBetweenValues
|
|
173
|
+
|
|
174
|
+
# remembers when the previous execution happened
|
|
175
|
+
previousTime = time.time() * 1000.0 # initialize to "now"...
|
|
176
|
+
|
|
177
|
+
# define an inner function (closure) that contains the logic of what to do
|
|
178
|
+
# with each provided value...
|
|
179
|
+
def processEachValueClosure():
|
|
180
|
+
|
|
181
|
+
nonlocal previousTime # remembers when previous execution happened
|
|
182
|
+
nonlocal valuesIndex # remembers which value is to be processed this time
|
|
183
|
+
nonlocal repeatCounter # remembers how many times we have processed the complete value list
|
|
184
|
+
|
|
185
|
+
# get current time (in millisecs)
|
|
186
|
+
currentTime = time.time() * 1000.0
|
|
187
|
+
|
|
188
|
+
# has enough time passed to trigger the next value?
|
|
189
|
+
if (currentTime - previousTime) >= delay:
|
|
190
|
+
|
|
191
|
+
# yes, update time tracker
|
|
192
|
+
previousTime = previousTime + delay
|
|
193
|
+
|
|
194
|
+
# act only if there are remaining values to be processed
|
|
195
|
+
if valuesIndex < len(values): # any left?
|
|
196
|
+
|
|
197
|
+
# yes, so...
|
|
198
|
+
|
|
199
|
+
# get next value
|
|
200
|
+
value = values[valuesIndex]
|
|
201
|
+
|
|
202
|
+
# NOTE: this is the important step that advances the animation!!!
|
|
203
|
+
action(value) # execute the original callback with the current value
|
|
204
|
+
|
|
205
|
+
# point to next value to be processed (if any)
|
|
206
|
+
valuesIndex = valuesIndex + 1
|
|
207
|
+
|
|
208
|
+
# now, the current value has been processed...
|
|
209
|
+
|
|
210
|
+
# have we completed processing all the values?
|
|
211
|
+
if valuesIndex >= len(values): # no more values left?
|
|
212
|
+
|
|
213
|
+
# yes, we are done... so, advance to the next repetition (if any)
|
|
214
|
+
repeatCounter = repeatCounter + 1
|
|
215
|
+
|
|
216
|
+
# do we have more repetitions?
|
|
217
|
+
if repeat == -1 or repeatCounter < repeat:
|
|
218
|
+
|
|
219
|
+
# yes, so start processing values once more
|
|
220
|
+
valuesIndex = 0 # point to first value
|
|
221
|
+
|
|
222
|
+
else: # we are done repeating, so...
|
|
223
|
+
|
|
224
|
+
# remove this inner function from the engine...
|
|
225
|
+
Animate.remove( processEachValueClosure )
|
|
226
|
+
|
|
227
|
+
# and execute the callback (if any)
|
|
228
|
+
if callable(whenDone): # is there a completion callback?
|
|
229
|
+
|
|
230
|
+
# yes, so execute it!!
|
|
231
|
+
whenDone()
|
|
232
|
+
|
|
233
|
+
# now, we have finished processing current value, and have taken care
|
|
234
|
+
# of anything else that may have needed to happen (i.e., start over next repetition,
|
|
235
|
+
# or called 'whenDone' function, if we are completely done - after this value)
|
|
236
|
+
|
|
237
|
+
# NOTE: Very important - now that this inner function has been built,
|
|
238
|
+
# we need to register it with our engine, so it can do its job...
|
|
239
|
+
|
|
240
|
+
# register the closure to be called - it will take care of everything else...
|
|
241
|
+
Animate._actionList.append(processEachValueClosure)
|
|
242
|
+
|
|
243
|
+
|
|
244
|
+
@staticmethod
|
|
245
|
+
def addWithTimedValues(action, values, times, duration=None, repeat=1, whenDone=None):
|
|
246
|
+
"""
|
|
247
|
+
Register 'action' (a callback function) to be called repeatedly.
|
|
248
|
+
'values' is a list of sublists containing the parameters to be passed to the action.
|
|
249
|
+
'times' is a parallel list containing the target timestamps (in millisecs) for each value.
|
|
250
|
+
This will be repeated 'repeat' times.
|
|
251
|
+
When this process is finished, function 'whenDone' is called (if provided).
|
|
252
|
+
"""
|
|
253
|
+
|
|
254
|
+
if not callable(action): # is it NOT a function?
|
|
255
|
+
|
|
256
|
+
# let them know that something is wrong...
|
|
257
|
+
print(f"Animate.addWithTimedValues(): '{action}' is not a callable function.")
|
|
258
|
+
|
|
259
|
+
elif len(values) == 0: # is the list empty?
|
|
260
|
+
|
|
261
|
+
# let them know that something is wrong...
|
|
262
|
+
print("Animate.addWithTimedValues(): 'values' list is empty.")
|
|
263
|
+
|
|
264
|
+
elif len(values) != len(times): # are the lists not parallel?
|
|
265
|
+
|
|
266
|
+
# let them know that something is wrong...
|
|
267
|
+
print(f"Animate.addWithTimedValues(): length of 'values' ({len(values)}) and 'times' ({len(times)}) must be the same.")
|
|
268
|
+
|
|
269
|
+
else: # lists are non-empty and parallel, so...
|
|
270
|
+
|
|
271
|
+
# calculate all raw intervals between consecutive times
|
|
272
|
+
# (if list has only 1 item, intervals will be empty)
|
|
273
|
+
intervals = [times[i] - times[i-1] for i in range(1, len(times))]
|
|
274
|
+
|
|
275
|
+
# check for simultaneous or out-of-order timestamps
|
|
276
|
+
if len(intervals) > 0 and min(intervals) <= 0:
|
|
277
|
+
|
|
278
|
+
# let them know that something is wrong...
|
|
279
|
+
print(f"Animate.addWithTimedValues(): all timestamps should be larger than their previous one.")
|
|
280
|
+
|
|
281
|
+
else: # timestamps are valid and strictly increasing, so check engine limits...
|
|
282
|
+
|
|
283
|
+
# initialize default time scale (no time-stretching)
|
|
284
|
+
timeScale = 1.0
|
|
285
|
+
|
|
286
|
+
if duration is not None: # did they provide duration?
|
|
287
|
+
|
|
288
|
+
# find the original duration (very last element in the times sequence)
|
|
289
|
+
originalDuration = float(times[-1])
|
|
290
|
+
|
|
291
|
+
if originalDuration > 0:
|
|
292
|
+
|
|
293
|
+
# calculate scale factor to stretch/compress the overall timing
|
|
294
|
+
timeScale = (duration * 1000.0) / originalDuration
|
|
295
|
+
|
|
296
|
+
# adjust the smallest raw interval by our timeScale to see what the engine will actually process
|
|
297
|
+
isTooSmall = False
|
|
298
|
+
|
|
299
|
+
if len(intervals) > 0:
|
|
300
|
+
|
|
301
|
+
minInterval = min(intervals) * timeScale
|
|
302
|
+
|
|
303
|
+
if minInterval < Animate._interval: # is the smallest gap too tight?
|
|
304
|
+
|
|
305
|
+
isTooSmall = True # remember that
|
|
306
|
+
|
|
307
|
+
# now, either isTooSmall is True, meaning the tightest gap is invalid, or
|
|
308
|
+
# it is False, meaning all intervals are valid
|
|
309
|
+
|
|
310
|
+
if isTooSmall: # did we find an interval that is too small?
|
|
311
|
+
|
|
312
|
+
# calculate what the scale factor WOULD need to be to make the smallest gap fit the engine
|
|
313
|
+
# (we can safely divide by min(intervals) here because we proved > 0 in the previous block)
|
|
314
|
+
requiredScaleFactor = Animate._interval / float(min(intervals))
|
|
315
|
+
|
|
316
|
+
# calculate the minimum allowable duration (in seconds) based on that scale factor
|
|
317
|
+
originalDuration = float(times[-1])
|
|
318
|
+
minAllowableDuration = (requiredScaleFactor * originalDuration) / 1000.0
|
|
319
|
+
|
|
320
|
+
if duration is not None: # did they provide a duration?
|
|
321
|
+
|
|
322
|
+
# let them know their duration is too short
|
|
323
|
+
print(f"Animate.addWithTimedValues(): duration ({duration} secs) is too small to process, given frame rate ({Animate.getRate()}).")
|
|
324
|
+
print(f" Should be at least {minAllowableDuration:.2f} secs.")
|
|
325
|
+
|
|
326
|
+
else: # they didn't provide a duration, but the raw timestamps are too tight
|
|
327
|
+
|
|
328
|
+
# let them know they need to stretch the sequence
|
|
329
|
+
print(f"Animate.addWithTimedValues(): found time interval that is too small ({minInterval} secs), given frame rate ({Animate.getRate()}).")
|
|
330
|
+
print(f" Increase interval between times, or provide a duration of at least {minAllowableDuration:.2f} secs.")
|
|
331
|
+
|
|
332
|
+
else: # all good, so proceed...
|
|
333
|
+
|
|
334
|
+
import time
|
|
335
|
+
|
|
336
|
+
#####
|
|
337
|
+
# Now, we will build a closure (inner function) to handle processing each individual value,
|
|
338
|
+
# as well as anything else that may need to happen (i.e., start over for the next repetition,
|
|
339
|
+
# or call the 'whenDone' function if we are completely done - after the last value).
|
|
340
|
+
|
|
341
|
+
# A closure needs some non-local (i.e., external to it) variables to remember how far it has gone.
|
|
342
|
+
|
|
343
|
+
# NOTE: Every time addWithTimedValues() above gets called, new variables are created here,
|
|
344
|
+
# and these are combined with the closure below, maintaining its state persistently
|
|
345
|
+
# without overwriting each other - it works well.
|
|
346
|
+
|
|
347
|
+
# external variables to be wrapped within the closure
|
|
348
|
+
valuesIndex = 0 # points to next value to be processed
|
|
349
|
+
repeatCounter = 0 # how many repetitions done so far
|
|
350
|
+
|
|
351
|
+
# remembers when the current animation sequence started
|
|
352
|
+
sequenceStartTime = time.time() * 1000.0 # initialize to "now"...
|
|
353
|
+
|
|
354
|
+
# define an inner function (closure) that contains the logic of what to do
|
|
355
|
+
# with each provided value...
|
|
356
|
+
def processEachTimedValueClosure():
|
|
357
|
+
|
|
358
|
+
nonlocal sequenceStartTime # remembers when the current sequence repetition started
|
|
359
|
+
nonlocal valuesIndex # remembers which value is to be processed this time
|
|
360
|
+
nonlocal repeatCounter # remembers how many times we have processed the complete value list
|
|
361
|
+
|
|
362
|
+
# get current time (in millisecs)
|
|
363
|
+
currentTime = time.time() * 1000.0
|
|
364
|
+
|
|
365
|
+
# calculate how much time has elapsed since the sequence started
|
|
366
|
+
elapsedTime = currentTime - sequenceStartTime
|
|
367
|
+
|
|
368
|
+
# get the target timestamp directly from the times list (adjusted by our timeScale)
|
|
369
|
+
targetTimestamp = times[valuesIndex] * timeScale
|
|
370
|
+
|
|
371
|
+
# has enough time passed to trigger the next value based on its target timestamp?
|
|
372
|
+
if elapsedTime >= targetTimestamp:
|
|
373
|
+
|
|
374
|
+
# yes, so...
|
|
375
|
+
|
|
376
|
+
# get the arguments directly from the values list
|
|
377
|
+
actionArgs = values[valuesIndex]
|
|
378
|
+
|
|
379
|
+
# NOTE: this is the important step that advances the animation!!!
|
|
380
|
+
# unpack the arguments (*) and execute the original callback
|
|
381
|
+
action(*actionArgs)
|
|
382
|
+
|
|
383
|
+
# point to next value to be processed (if any)
|
|
384
|
+
valuesIndex = valuesIndex + 1
|
|
385
|
+
|
|
386
|
+
# now, the current value has been processed...
|
|
387
|
+
|
|
388
|
+
# have we completed processing all the values?
|
|
389
|
+
if valuesIndex >= len(values): # no more values left?
|
|
390
|
+
|
|
391
|
+
# yes, we are done... so, advance to the next repetition (if any)
|
|
392
|
+
repeatCounter = repeatCounter + 1
|
|
393
|
+
|
|
394
|
+
# do we have more repetitions?
|
|
395
|
+
if repeat == -1 or repeatCounter < repeat:
|
|
396
|
+
|
|
397
|
+
# yes, so start processing values once more
|
|
398
|
+
valuesIndex = 0 # point to first value
|
|
399
|
+
|
|
400
|
+
# reset the sequence start time for the next repetition cycle
|
|
401
|
+
sequenceStartTime = time.time() * 1000.0
|
|
402
|
+
|
|
403
|
+
else: # we are done repeating, so...
|
|
404
|
+
|
|
405
|
+
# remove this inner function from the engine...
|
|
406
|
+
Animate.remove( processEachTimedValueClosure )
|
|
407
|
+
|
|
408
|
+
# and execute the callback (if any)
|
|
409
|
+
if callable(whenDone): # is there a completion callback?
|
|
410
|
+
|
|
411
|
+
# yes, so execute it!!
|
|
412
|
+
whenDone()
|
|
413
|
+
|
|
414
|
+
# now, we have finished processing current value, and have taken care
|
|
415
|
+
# of anything else that may have needed to happen (i.e., start over next repetition,
|
|
416
|
+
# or called 'whenDone' function, if we are completely done - after this value)
|
|
417
|
+
|
|
418
|
+
# NOTE: Very important - now that this inner function has been built,
|
|
419
|
+
# we need to register it with our engine, so it can do its job...
|
|
420
|
+
|
|
421
|
+
# register the closure to be called - it will take care of everything else...
|
|
422
|
+
Animate._actionList.append(processEachTimedValueClosure)
|
|
423
|
+
|
|
424
|
+
|
|
425
|
+
@staticmethod
|
|
426
|
+
def remove(action):
|
|
427
|
+
"""
|
|
428
|
+
Remove a callback function from the list of functions being called.
|
|
429
|
+
If a function is registered several times, it removes the earliest addition.
|
|
430
|
+
"""
|
|
431
|
+
|
|
432
|
+
if action in Animate._actionList: # is this a previously registered function?
|
|
433
|
+
|
|
434
|
+
# yes, so remove first matching occurrence
|
|
435
|
+
Animate._actionList.remove(action)
|
|
436
|
+
|
|
437
|
+
else:
|
|
438
|
+
|
|
439
|
+
# let them know that something is wrong...
|
|
440
|
+
print(f"Animate.remove(): '{action}' does not appear in the list of registered functions.")
|
|
441
|
+
|
|
442
|
+
|
|
443
|
+
@staticmethod
|
|
444
|
+
def getRate():
|
|
445
|
+
"""
|
|
446
|
+
Return current animation rate (in frames per second).
|
|
447
|
+
"""
|
|
448
|
+
|
|
449
|
+
# convert milliseconds back to frames per second
|
|
450
|
+
return int(1000.0 / Animate._interval)
|
|
451
|
+
|
|
452
|
+
|
|
453
|
+
@staticmethod
|
|
454
|
+
def setRate(frameRate=60):
|
|
455
|
+
"""
|
|
456
|
+
Set current animation rate (in frames per second).
|
|
457
|
+
"""
|
|
458
|
+
|
|
459
|
+
# convert frames per second to milliseconds
|
|
460
|
+
Animate._interval = (1000.0 / frameRate)
|
|
461
|
+
|
|
462
|
+
if Animate._ENGINE is not None: # do we have an engine?
|
|
463
|
+
|
|
464
|
+
# yes, so update the engine
|
|
465
|
+
Animate._ENGINE.setDelay(Animate._interval)
|
|
466
|
+
|
|
467
|
+
|
|
468
|
+
#####
|
|
469
|
+
# NOTE: This is important - initialize engine and start master timer at module load time.
|
|
470
|
+
# Leave it here!!!
|
|
471
|
+
Animate._init()
|
|
472
|
+
|
|
473
|
+
|
|
474
|
+
####
|
|
475
|
+
# function aliases for backwards compatibility / simplicity
|
|
476
|
+
def animate(action):
|
|
477
|
+
"""
|
|
478
|
+
Add a function to be called repeatedly by animation engine.
|
|
479
|
+
"""
|
|
480
|
+
|
|
481
|
+
Animate.add(action)
|
|
482
|
+
|
|
483
|
+
|
|
484
|
+
def setAnimationRate(frameRate=60):
|
|
485
|
+
"""
|
|
486
|
+
Set animation frame rate (frames per second).
|
|
487
|
+
"""
|
|
488
|
+
|
|
489
|
+
Animate.setRate(frameRate)
|
|
490
|
+
|
|
491
|
+
|
|
492
|
+
def getAnimationRate():
|
|
493
|
+
"""
|
|
494
|
+
Return current animation frame rate (frames per second).
|
|
495
|
+
"""
|
|
496
|
+
|
|
497
|
+
return Animate.getRate()
|
|
498
|
+
|
|
499
|
+
|
|
500
|
+
def pauseAnimation():
|
|
501
|
+
"""
|
|
502
|
+
Pause animation (everything else remains in place).
|
|
503
|
+
"""
|
|
504
|
+
Animate.pause()
|
|
505
|
+
|
|
506
|
+
|
|
507
|
+
def resumeAnimation():
|
|
508
|
+
"""
|
|
509
|
+
Resume stopped animation (everything continues from where it had left off).
|
|
510
|
+
"""
|
|
511
|
+
|
|
512
|
+
Animate.resume()
|
|
513
|
+
|
|
514
|
+
|
|
515
|
+
####
|
|
516
|
+
# Helper functions
|
|
517
|
+
|
|
518
|
+
from music import mapValue # import mapValue from music library
|
|
519
|
+
|
|
520
|
+
def mapValueList(values, minValue, maxValue, minResult, maxResult):
|
|
521
|
+
"""
|
|
522
|
+
Map a sequence of 'values' from their original range (defined by 'minValue'
|
|
523
|
+
and 'maxValue') to a new range defined by 'minResult' and 'maxResult'.
|
|
524
|
+
Returns a new list containing the mapped values.
|
|
525
|
+
"""
|
|
526
|
+
|
|
527
|
+
newValues = [] # holds mapped values
|
|
528
|
+
|
|
529
|
+
# do some sanity checking...
|
|
530
|
+
if type(values) is not list: # are the provided values NOT in a list?
|
|
531
|
+
|
|
532
|
+
# let them know something is wrong...
|
|
533
|
+
print(f"mapValueList(): '{values}' is not a valid list.")
|
|
534
|
+
|
|
535
|
+
elif len(values) == 0: # is the list empty?
|
|
536
|
+
|
|
537
|
+
# let them know something is wrong...
|
|
538
|
+
print("mapValueList(): 'trajectoryValues' list is empty.")
|
|
539
|
+
|
|
540
|
+
else: # all good, so proceed...
|
|
541
|
+
|
|
542
|
+
# map each value in provided list
|
|
543
|
+
for value in values:
|
|
544
|
+
|
|
545
|
+
# map this value from source to target range
|
|
546
|
+
newValue = mapValue(value, minValue, maxValue, minResult, maxResult)
|
|
547
|
+
|
|
548
|
+
# and remember it...
|
|
549
|
+
newValues.append(newValue)
|
|
550
|
+
|
|
551
|
+
# now, newValues contains scaled values...
|
|
552
|
+
|
|
553
|
+
# and return it (empty, if validation checks fail)
|
|
554
|
+
return newValues
|