Table of Contents
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
Rotating Fractal Loop
Fractal noise with smooth rotation that completes full revolutions over 600 frames (10 seconds at 60fps).
Loop Guarantee
Rotation completes exactly 360 degrees over 600 frames, returning to original orientation.
Mathematical Formula
\text{fractal} = \sum_{i=0}^{N} 2^{-i} \cdot n(2^i \cdot x)
\text{angle} = \frac{t}{\text{total\_frames}} \cdot 360^\circ
\text{rotated} = \text{rotate}(\text{fractal}, \text{angle})
Where:
\text{fractal}is multi-scale noise\text{angle}increases linearly from0^\circto360^\circ\text{rotate}()performs90^\circincrements
How It Works
This pattern combines fractal noise with rotation:
- Generate multi-scale noise (fractal Brownian motion)
- Map to psychedelic color palette
- Rotate by an angle that increases linearly with time
- Complete exactly 360° over the total frame count
Implementation
import torch
from scipy.ndimage import gaussian_filter
import numpy as np
width, height = 512, 512
fps = 60
duration = 10
total_frames = fps * duration
frames = []
for t in range(total_frames):
# Generate noise at multiple scales
noise_layers = []
for scale in [4, 8, 16, 32]:
noise = torch.rand(height//scale, width//scale).numpy()
noise = gaussian_filter(noise, sigma=2)
noise = torch.tensor(noise).unsqueeze(-1).repeat(1, 1, 3)
noise = torch.nn.functional.interpolate(
noise.unsqueeze(0),
size=(height, width),
mode='bilinear'
).squeeze(0)
noise_layers.append(noise)
fractal = sum(noise_layers) / len(noise_layers)
# Map to psychedelic palette
r = fractal[:, :, 0] * 0.6 + fractal[:, :, 1] * 0.4
g = fractal[:, :, 1] * 0.5 + fractal[:, :, 2] * 0.5
b = fractal[:, :, 2] * 0.7 + fractal[:, :, 0] * 0.3
rgb = torch.stack([r, g, b], dim=-1).clamp(0, 1)
# Rotation: complete 1 full revolution over 600 frames
angle = (t / total_frames) * 360
k = int(angle / 90)
rgb = torch.rot90(rgb, k=k)
frames.append(rgb)
output_image = torch.stack(frames, dim=0)
Line-by-Line Explanation
angle = (t / total_frames) * 360
Angle increases linearly from 0° at t=0 to 360° at t=600.
k = int(angle / 90)
Converts angle to number of 90° increments. torch.rot90() only rotates by 90° increments.
rgb = torch.rot90(rgb, k=k)
Rotates the image by k × 90°. After 600 frames, k = 4, which is equivalent to 360° (full revolution).
Mathematical Insight
Why Linear Angle Increase?
angle = (t / total_frames) * 360
This ensures:
- At t=0: angle = 0°
- At t=600: angle = 360°
- At t=300: angle = 180° (halfway through)
Linear increase creates smooth, constant-speed rotation.
Discrete Rotation
torch.rot90() only rotates by 90° increments. We approximate smooth rotation by:
- Calculating the desired angle
- Converting to 90° increments
- Snapping to the nearest increment
For 600 frames:
- Frames 0-22: k=0 (0°)
- Frames 23-67: k=1 (90°)
- Frames 68-112: k=2 (180°)
- Frames 113-157: k=3 (270°)
- Frames 158-202: k=4 (360° ≡ 0°)
- And so on...
This creates a stepped rotation rather than perfectly smooth rotation, but it loops perfectly.
Customization
Multiple Revolutions
angle = (t / total_frames) * 720 # 2 full revolutions
Different Octaves
for scale in [2, 4, 8, 16, 32, 64]: # 6 octaves
Different Color Palette
# Oceanic
r = fractal[:, :, 2] * 0.7 + fractal[:, :, 1] * 0.3
g = fractal[:, :, 1] * 0.5 + fractal[:, :, 0] * 0.5
b = fractal[:, :, 0] * 0.6 + fractal[:, :, 2] * 0.4
No Rotation
# Remove rotation lines for static fractal
Performance Notes
- Fractal generation is the bottleneck
- Gaussian filtering is expensive
- Rotation is fast
- Consider fewer octaves for real-time
Loop Verification
# After generating frames
first_frame = output_image[0]
last_frame = output_image[-1]
diff = torch.abs(first_frame - last_frame).max()
print(f"Max difference: {diff.item()}")
# Should be 0 (exact match due to discrete rotation)