2 patterns-earthbound-Rotating-Fractal-Loop
Balazs Horvath edited this page 2026-04-18 11:13:13 +02:00
This file contains ambiguous Unicode characters

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 from 0^\circ to 360^\circ
  • \text{rotate}() performs 90^\circ increments

How It Works

This pattern combines fractal noise with rotation:

  1. Generate multi-scale noise (fractal Brownian motion)
  2. Map to psychedelic color palette
  3. Rotate by an angle that increases linearly with time
  4. 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:

  1. Calculating the desired angle
  2. Converting to 90° increments
  3. 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)