212 lines
6.2 KiB
Python
Executable File
212 lines
6.2 KiB
Python
Executable File
#!/usr/bin/env python3
|
|
"""
|
|
Kaleidoscope Effect - Create mirror/rotation effects.
|
|
|
|
Apply kaleidoscope effects to frames or objects for psychedelic visuals.
|
|
"""
|
|
|
|
import sys
|
|
from pathlib import Path
|
|
import math
|
|
|
|
sys.path.append(str(Path(__file__).parent.parent))
|
|
|
|
from PIL import Image, ImageOps, ImageDraw
|
|
import numpy as np
|
|
|
|
|
|
def apply_kaleidoscope(frame: Image.Image, segments: int = 8,
|
|
center: tuple[int, int] | None = None) -> Image.Image:
|
|
"""
|
|
Apply kaleidoscope effect by mirroring/rotating frame sections.
|
|
|
|
Args:
|
|
frame: Input frame
|
|
segments: Number of mirror segments (4, 6, 8, 12 work well)
|
|
center: Center point for effect (None = frame center)
|
|
|
|
Returns:
|
|
Frame with kaleidoscope effect
|
|
"""
|
|
width, height = frame.size
|
|
|
|
if center is None:
|
|
center = (width // 2, height // 2)
|
|
|
|
# Create output frame
|
|
output = Image.new('RGB', (width, height))
|
|
|
|
# Calculate angle per segment
|
|
angle_per_segment = 360 / segments
|
|
|
|
# For simplicity, we'll create a radial mirror effect
|
|
# A full implementation would rotate and mirror properly
|
|
# This is a simplified version that creates interesting patterns
|
|
|
|
# Convert to numpy for easier manipulation
|
|
frame_array = np.array(frame)
|
|
output_array = np.zeros_like(frame_array)
|
|
|
|
center_x, center_y = center
|
|
|
|
# Create wedge mask and mirror it
|
|
for y in range(height):
|
|
for x in range(width):
|
|
# Calculate angle from center
|
|
dx = x - center_x
|
|
dy = y - center_y
|
|
|
|
angle = (math.degrees(math.atan2(dy, dx)) + 180) % 360
|
|
distance = math.sqrt(dx * dx + dy * dy)
|
|
|
|
# Which segment does this pixel belong to?
|
|
segment = int(angle / angle_per_segment)
|
|
|
|
# Mirror angle within segment
|
|
segment_angle = angle % angle_per_segment
|
|
if segment % 2 == 1: # Mirror every other segment
|
|
segment_angle = angle_per_segment - segment_angle
|
|
|
|
# Calculate source position
|
|
source_angle = segment_angle + (segment // 2) * angle_per_segment * 2
|
|
source_angle_rad = math.radians(source_angle - 180)
|
|
|
|
source_x = int(center_x + distance * math.cos(source_angle_rad))
|
|
source_y = int(center_y + distance * math.sin(source_angle_rad))
|
|
|
|
# Bounds check
|
|
if 0 <= source_x < width and 0 <= source_y < height:
|
|
output_array[y, x] = frame_array[source_y, source_x]
|
|
else:
|
|
output_array[y, x] = frame_array[y, x]
|
|
|
|
return Image.fromarray(output_array)
|
|
|
|
|
|
def apply_simple_mirror(frame: Image.Image, mode: str = 'quad') -> Image.Image:
|
|
"""
|
|
Apply simple mirror effect (faster than full kaleidoscope).
|
|
|
|
Args:
|
|
frame: Input frame
|
|
mode: 'horizontal', 'vertical', 'quad' (4-way), 'radial'
|
|
|
|
Returns:
|
|
Mirrored frame
|
|
"""
|
|
width, height = frame.size
|
|
center_x, center_y = width // 2, height // 2
|
|
|
|
if mode == 'horizontal':
|
|
# Mirror left half to right
|
|
left_half = frame.crop((0, 0, center_x, height))
|
|
left_flipped = ImageOps.mirror(left_half)
|
|
result = frame.copy()
|
|
result.paste(left_flipped, (center_x, 0))
|
|
return result
|
|
|
|
elif mode == 'vertical':
|
|
# Mirror top half to bottom
|
|
top_half = frame.crop((0, 0, width, center_y))
|
|
top_flipped = ImageOps.flip(top_half)
|
|
result = frame.copy()
|
|
result.paste(top_flipped, (0, center_y))
|
|
return result
|
|
|
|
elif mode == 'quad':
|
|
# 4-way mirror (top-left quadrant mirrored to all)
|
|
quad = frame.crop((0, 0, center_x, center_y))
|
|
|
|
result = Image.new('RGB', (width, height))
|
|
|
|
# Top-left (original)
|
|
result.paste(quad, (0, 0))
|
|
|
|
# Top-right (horizontal mirror)
|
|
result.paste(ImageOps.mirror(quad), (center_x, 0))
|
|
|
|
# Bottom-left (vertical mirror)
|
|
result.paste(ImageOps.flip(quad), (0, center_y))
|
|
|
|
# Bottom-right (both mirrors)
|
|
result.paste(ImageOps.flip(ImageOps.mirror(quad)), (center_x, center_y))
|
|
|
|
return result
|
|
|
|
else:
|
|
return frame
|
|
|
|
|
|
def create_kaleidoscope_animation(
|
|
base_frame: Image.Image | None = None,
|
|
num_frames: int = 30,
|
|
segments: int = 8,
|
|
rotation_speed: float = 1.0,
|
|
width: int = 480,
|
|
height: int = 480
|
|
) -> list[Image.Image]:
|
|
"""
|
|
Create animated kaleidoscope effect.
|
|
|
|
Args:
|
|
base_frame: Frame to apply effect to (or None for demo pattern)
|
|
num_frames: Number of frames
|
|
segments: Kaleidoscope segments
|
|
rotation_speed: How fast pattern rotates (0.5-2.0)
|
|
width: Frame width if generating demo
|
|
height: Frame height if generating demo
|
|
|
|
Returns:
|
|
List of frames with kaleidoscope effect
|
|
"""
|
|
frames = []
|
|
|
|
# Create demo pattern if no base frame
|
|
if base_frame is None:
|
|
base_frame = Image.new('RGB', (width, height), (255, 255, 255))
|
|
draw = ImageDraw.Draw(base_frame)
|
|
|
|
# Draw some colored shapes
|
|
from core.color_palettes import get_palette
|
|
palette = get_palette('vibrant')
|
|
|
|
colors = [palette['primary'], palette['secondary'], palette['accent']]
|
|
|
|
for i, color in enumerate(colors):
|
|
x = width // 2 + int(100 * math.cos(i * 2 * math.pi / 3))
|
|
y = height // 2 + int(100 * math.sin(i * 2 * math.pi / 3))
|
|
draw.ellipse([x - 40, y - 40, x + 40, y + 40], fill=color)
|
|
|
|
# Rotate base frame and apply kaleidoscope
|
|
for i in range(num_frames):
|
|
angle = (i / num_frames) * 360 * rotation_speed
|
|
|
|
# Rotate base frame
|
|
rotated = base_frame.rotate(angle, resample=Image.BICUBIC)
|
|
|
|
# Apply kaleidoscope
|
|
kaleido_frame = apply_kaleidoscope(rotated, segments=segments)
|
|
|
|
frames.append(kaleido_frame)
|
|
|
|
return frames
|
|
|
|
|
|
# Example usage
|
|
if __name__ == '__main__':
|
|
from core.gif_builder import GIFBuilder
|
|
|
|
print("Creating kaleidoscope GIF...")
|
|
|
|
builder = GIFBuilder(width=480, height=480, fps=20)
|
|
|
|
# Create kaleidoscope animation
|
|
frames = create_kaleidoscope_animation(
|
|
num_frames=40,
|
|
segments=8,
|
|
rotation_speed=0.5
|
|
)
|
|
|
|
builder.add_frames(frames)
|
|
builder.save('kaleidoscope_test.gif', num_colors=128)
|