-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathrotation.py
More file actions
207 lines (157 loc) · 6.62 KB
/
rotation.py
File metadata and controls
207 lines (157 loc) · 6.62 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
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
#!/usr/bin/env python3
"""
Rotation Module
===============
Rotates the entire drawing surface during the animation.
This is a TRANSFORMER module - it takes input coordinates and rotates them
around a specified center point based on the time parameter.
This simulates slowly spinning the paper (or the entire spirograph apparatus)
while drawing, creating spiral or rosette-like patterns from simpler curves.
The rotation is applied as:
z' = center + (z - center) * e^(iθ(t))
Where θ(t) increases linearly from 0 to total_rotation as t goes from 0 to 1.
"""
import numpy as np
from fractions import Fraction
from math import pi
from main import TransformModule
class RotationModule(TransformModule):
"""
Surface rotation: rotates input coordinates around a center point.
This is a TRANSFORMER module - it modifies input z based on time t.
Configuration:
total_degrees: Total rotation over the full drawing in degrees
origin_x, origin_y: Center of rotation (default 0,0)
normalize: If true (default), normalize t to [0,1] regardless of pipeline period
"""
def _load_config(self):
"""Load rotation configuration."""
self.total_degrees = self._getfloat('total_degrees', 360.0)
self.origin_x = self._getfloat('origin_x', 0.0)
self.origin_y = self._getfloat('origin_y', 0.0)
self.normalize = self._getboolean('normalize', True)
# Convert to radians
self.total_radians = self.total_degrees * pi / 180
# Rotation center as complex number
self.origin = self.origin_x + 1j * self.origin_y
# Determine the period based on rotation amount
self._compute_period()
def _compute_period(self):
"""Compute the natural period based on rotation."""
# Express total rotation as a fraction of full circles
full_circles = abs(self.total_degrees) / 360.0
if full_circles == 0:
self._period = Fraction(1, 1)
else:
self._period = Fraction(full_circles).limit_denominator(1000)
def transform(self, z: complex, t: float) -> complex:
"""
Rotate input coordinates around the origin point.
Args:
z: Input position to transform
t: Time parameter in [0, 1] or [0, period]
Returns:
Rotated position
"""
t_use = self._normalize_t(t)
# Current rotation angle
theta = t_use * self.total_radians
# Rotation factor
rotation = np.exp(1j * theta)
# Rotate around origin: z' = origin + (z - origin) * rotation
relative = z - self.origin
rotated = relative * rotation
result = self.origin + rotated
return result
@property
def natural_period(self) -> Fraction:
"""Period of the rotation."""
return self._period
@property
def is_generator(self) -> bool:
"""This module transforms coordinates, not generates."""
return False
def __repr__(self):
if self.origin_x == 0 and self.origin_y == 0:
return f"RotationModule({self.total_degrees}° around origin)"
return f"RotationModule({self.total_degrees}° around ({self.origin_x}, {self.origin_y}))"
class OscillatingRotationModule(TransformModule):
"""
Oscillating rotation: rotates back and forth instead of continuously.
This creates a pendulum-like motion, useful for creating symmetric patterns.
Configuration:
amplitude_degrees: Maximum rotation angle in each direction
oscillations: Number of complete back-and-forth cycles
rotate_around_origin: If true, rotate around (0,0)
center_x, center_y: Center of rotation (if not origin)
normalize: If true (default), normalize t to [0,1] regardless of pipeline period
"""
def _load_config(self):
"""Load oscillation configuration."""
self.amplitude_degrees = self._getfloat('amplitude_degrees', 45.0)
self.oscillations = self._getfloat('oscillations', 1.0)
self.rotate_around_origin = self._getboolean('rotate_around_origin', True)
self.center_x = self._getfloat('center_x', 0.0)
self.center_y = self._getfloat('center_y', 0.0)
self.normalize = self._getboolean('normalize', True)
# Convert to radians
self.amplitude_radians = self.amplitude_degrees * pi / 180
# Rotation center
if self.rotate_around_origin:
self.center = 0 + 0j
else:
self.center = self.center_x + 1j * self.center_y
def transform(self, z: complex, t: float) -> complex:
"""
Apply oscillating rotation to input coordinates.
Uses a sinusoidal rotation angle.
Args:
z: Input position to transform
t: Time parameter in [0, 1] or [0, period]
Returns:
Rotated position
"""
t_use = self._normalize_t(t)
# Oscillating angle using sine wave
theta = self.amplitude_radians * np.sin(2 * pi * self.oscillations * t_use)
# Rotation factor
rotation = np.exp(1j * theta)
# Rotate around center
relative = z - self.center
rotated = relative * rotation
result = self.center + rotated
return result
@property
def natural_period(self) -> Fraction:
"""Period matches the oscillation count."""
return Fraction(self.oscillations).limit_denominator(1000)
@property
def is_generator(self) -> bool:
"""This module transforms coordinates."""
return False
def __repr__(self):
return f"OscillatingRotationModule(±{self.amplitude_degrees}°, {self.oscillations} cycles)"
# Convenience function for standalone testing
def _test():
"""Quick visual test of the module."""
import configparser
config = configparser.ConfigParser()
config.read_string("""
[rotation]
total_degrees = 360.0
center_x = 0.0
center_y = 0.0
rotate_around_origin = true
""")
module = RotationModule(config, 'rotation')
print(module)
print(f"Natural period: {module.natural_period}")
# Test with a point at (1, 0)
test_point = 1 + 0j
print(f"\nRotating point {test_point}:")
for t in [0.0, 0.125, 0.25, 0.375, 0.5, 0.625, 0.75, 0.875, 1.0]:
z = module.transform(test_point, t)
angle = np.angle(z) * 180 / pi
print(f" t={t:.3f}: ({z.real:8.5f}, {z.imag:8.5f}) angle={angle:7.2f}°")
if __name__ == "__main__":
_test()