main
1#!/usr/bin/env python3
2"""
3Kaleidoscope Effect - Create mirror/rotation effects.
4
5Apply kaleidoscope effects to frames or objects for psychedelic visuals.
6"""
7
8import sys
9from pathlib import Path
10import math
11
12sys.path.append(str(Path(__file__).parent.parent))
13
14from PIL import Image, ImageOps, ImageDraw
15import numpy as np
16
17
18def apply_kaleidoscope(frame: Image.Image, segments: int = 8,
19 center: tuple[int, int] | None = None) -> Image.Image:
20 """
21 Apply kaleidoscope effect by mirroring/rotating frame sections.
22
23 Args:
24 frame: Input frame
25 segments: Number of mirror segments (4, 6, 8, 12 work well)
26 center: Center point for effect (None = frame center)
27
28 Returns:
29 Frame with kaleidoscope effect
30 """
31 width, height = frame.size
32
33 if center is None:
34 center = (width // 2, height // 2)
35
36 # Create output frame
37 output = Image.new('RGB', (width, height))
38
39 # Calculate angle per segment
40 angle_per_segment = 360 / segments
41
42 # For simplicity, we'll create a radial mirror effect
43 # A full implementation would rotate and mirror properly
44 # This is a simplified version that creates interesting patterns
45
46 # Convert to numpy for easier manipulation
47 frame_array = np.array(frame)
48 output_array = np.zeros_like(frame_array)
49
50 center_x, center_y = center
51
52 # Create wedge mask and mirror it
53 for y in range(height):
54 for x in range(width):
55 # Calculate angle from center
56 dx = x - center_x
57 dy = y - center_y
58
59 angle = (math.degrees(math.atan2(dy, dx)) + 180) % 360
60 distance = math.sqrt(dx * dx + dy * dy)
61
62 # Which segment does this pixel belong to?
63 segment = int(angle / angle_per_segment)
64
65 # Mirror angle within segment
66 segment_angle = angle % angle_per_segment
67 if segment % 2 == 1: # Mirror every other segment
68 segment_angle = angle_per_segment - segment_angle
69
70 # Calculate source position
71 source_angle = segment_angle + (segment // 2) * angle_per_segment * 2
72 source_angle_rad = math.radians(source_angle - 180)
73
74 source_x = int(center_x + distance * math.cos(source_angle_rad))
75 source_y = int(center_y + distance * math.sin(source_angle_rad))
76
77 # Bounds check
78 if 0 <= source_x < width and 0 <= source_y < height:
79 output_array[y, x] = frame_array[source_y, source_x]
80 else:
81 output_array[y, x] = frame_array[y, x]
82
83 return Image.fromarray(output_array)
84
85
86def apply_simple_mirror(frame: Image.Image, mode: str = 'quad') -> Image.Image:
87 """
88 Apply simple mirror effect (faster than full kaleidoscope).
89
90 Args:
91 frame: Input frame
92 mode: 'horizontal', 'vertical', 'quad' (4-way), 'radial'
93
94 Returns:
95 Mirrored frame
96 """
97 width, height = frame.size
98 center_x, center_y = width // 2, height // 2
99
100 if mode == 'horizontal':
101 # Mirror left half to right
102 left_half = frame.crop((0, 0, center_x, height))
103 left_flipped = ImageOps.mirror(left_half)
104 result = frame.copy()
105 result.paste(left_flipped, (center_x, 0))
106 return result
107
108 elif mode == 'vertical':
109 # Mirror top half to bottom
110 top_half = frame.crop((0, 0, width, center_y))
111 top_flipped = ImageOps.flip(top_half)
112 result = frame.copy()
113 result.paste(top_flipped, (0, center_y))
114 return result
115
116 elif mode == 'quad':
117 # 4-way mirror (top-left quadrant mirrored to all)
118 quad = frame.crop((0, 0, center_x, center_y))
119
120 result = Image.new('RGB', (width, height))
121
122 # Top-left (original)
123 result.paste(quad, (0, 0))
124
125 # Top-right (horizontal mirror)
126 result.paste(ImageOps.mirror(quad), (center_x, 0))
127
128 # Bottom-left (vertical mirror)
129 result.paste(ImageOps.flip(quad), (0, center_y))
130
131 # Bottom-right (both mirrors)
132 result.paste(ImageOps.flip(ImageOps.mirror(quad)), (center_x, center_y))
133
134 return result
135
136 else:
137 return frame
138
139
140def create_kaleidoscope_animation(
141 base_frame: Image.Image | None = None,
142 num_frames: int = 30,
143 segments: int = 8,
144 rotation_speed: float = 1.0,
145 width: int = 480,
146 height: int = 480
147) -> list[Image.Image]:
148 """
149 Create animated kaleidoscope effect.
150
151 Args:
152 base_frame: Frame to apply effect to (or None for demo pattern)
153 num_frames: Number of frames
154 segments: Kaleidoscope segments
155 rotation_speed: How fast pattern rotates (0.5-2.0)
156 width: Frame width if generating demo
157 height: Frame height if generating demo
158
159 Returns:
160 List of frames with kaleidoscope effect
161 """
162 frames = []
163
164 # Create demo pattern if no base frame
165 if base_frame is None:
166 base_frame = Image.new('RGB', (width, height), (255, 255, 255))
167 draw = ImageDraw.Draw(base_frame)
168
169 # Draw some colored shapes
170 from core.color_palettes import get_palette
171 palette = get_palette('vibrant')
172
173 colors = [palette['primary'], palette['secondary'], palette['accent']]
174
175 for i, color in enumerate(colors):
176 x = width // 2 + int(100 * math.cos(i * 2 * math.pi / 3))
177 y = height // 2 + int(100 * math.sin(i * 2 * math.pi / 3))
178 draw.ellipse([x - 40, y - 40, x + 40, y + 40], fill=color)
179
180 # Rotate base frame and apply kaleidoscope
181 for i in range(num_frames):
182 angle = (i / num_frames) * 360 * rotation_speed
183
184 # Rotate base frame
185 rotated = base_frame.rotate(angle, resample=Image.BICUBIC)
186
187 # Apply kaleidoscope
188 kaleido_frame = apply_kaleidoscope(rotated, segments=segments)
189
190 frames.append(kaleido_frame)
191
192 return frames
193
194
195# Example usage
196if __name__ == '__main__':
197 from core.gif_builder import GIFBuilder
198
199 print("Creating kaleidoscope GIF...")
200
201 builder = GIFBuilder(width=480, height=480, fps=20)
202
203 # Create kaleidoscope animation
204 frames = create_kaleidoscope_animation(
205 num_frames=40,
206 segments=8,
207 rotation_speed=0.5
208 )
209
210 builder.add_frames(frames)
211 builder.save('kaleidoscope_test.gif', num_colors=128)