-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathlissajous.py
More file actions
95 lines (75 loc) · 3.31 KB
/
lissajous.py
File metadata and controls
95 lines (75 loc) · 3.31 KB
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
#!/usr/bin/env python3
"""
Lissajous Module
================
Draws Lissajous curves: x = A*sin(a*t + δ), y = B*sin(b*t)
These are the patterns seen on oscilloscopes when two
sine waves are combined. Related to spirograph patterns.
Famous examples:
- a=1, b=2: figure-8
- a=3, b=2: pretzel
- a=5, b=4: complex knot
"""
import numpy as np
from fractions import Fraction
from math import pi, gcd
from main import TransformModule
class LissajousModule(TransformModule):
"""
Lissajous curve generator.
Configuration:
freq_x, freq_y: Frequency ratio (integers)
amplitude_x, amplitude_y: Amplitudes
phase: Phase difference in degrees
cycles: Number of complete cycles (0 = auto)
start_x, start_y: Center position
"""
def _load_config(self):
"""Load Lissajous configuration."""
self.freq_x = self._getint('freq_x', 3)
self.freq_y = self._getint('freq_y', 2)
self.amplitude_x = self._getfloat('amplitude_x', 50.0)
self.amplitude_y = self._getfloat('amplitude_y', 50.0)
self.end_amplitude_x = self._getfloat('end_amplitude_x', self.amplitude_x)
self.end_amplitude_y = self._getfloat('end_amplitude_y', self.amplitude_y)
self.phase_deg = self._getfloat('phase', 90.0)
self.end_phase_deg = self._getfloat('end_phase', self.phase_deg)
self.cycles = self._getfloat('cycles', 0)
self.phase_rad = self.phase_deg * pi / 180
self.end_phase_rad = self.end_phase_deg * pi / 180
# Calculate closure cycles (for one complete Lissajous)
g = gcd(self.freq_x, self.freq_y)
self._closure_cycles = self.freq_y // g
# If cycles not specified, default to one complete figure
if self.cycles <= 0:
self.cycles = 1.0
def transform(self, z: complex, t: float) -> complex:
"""
Generate point on Lissajous curve at time t.
With cycles > 1, draws the figure multiple times.
Combined with transforms, creates moiré effects.
"""
# Normalize t to [0,1] for global interpolation
period = float(self._pipeline_period)
t_norm = t / period if period > 0 else t
# Convert to position within cycles
t_in_cycles = t_norm * self.cycles
# Position within current cycle [0, 1)
t_frac = t_in_cycles % 1.0
# Interpolate amplitudes based on overall progress
ax = self._interpolate(self.amplitude_x, self.end_amplitude_x, t_norm, 'amplitude_x')
ay = self._interpolate(self.amplitude_y, self.end_amplitude_y, t_norm, 'amplitude_y')
# Parameter for this single Lissajous trace
theta = t_frac * self._closure_cycles * 2 * pi
# Lissajous equations
phase = self._interpolate(self.phase_rad, self.end_phase_rad, t_norm, 'phase')
x = ax * np.sin(self.freq_x * theta + phase)
y = ay * np.sin(self.freq_y * theta)
point = x + 1j * y
return z + point
@property
def natural_period(self) -> Fraction:
"""Period based on cycles."""
return Fraction(self.cycles).limit_denominator(1000)
def __repr__(self):
return f"LissajousModule({self.freq_x}:{self.freq_y}, A=({self.amplitude_x}, {self.amplitude_y}))"