daemora 1.0.3 → 1.0.5
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +663 -0
- package/README.md +69 -19
- package/SOUL.md +25 -24
- package/daemora-ui/README.md +11 -0
- package/package.json +12 -2
- package/skills/api-development.md +35 -0
- package/skills/artifacts-builder/SKILL.md +74 -0
- package/skills/artifacts-builder/scripts/bundle-artifact.sh +54 -0
- package/skills/artifacts-builder/scripts/init-artifact.sh +322 -0
- package/skills/artifacts-builder/scripts/shadcn-components.tar.gz +0 -0
- package/skills/brand-guidelines.md +73 -0
- package/skills/browser.md +77 -0
- package/skills/changelog-generator.md +104 -0
- package/skills/coding.md +26 -10
- package/skills/content-research-writer.md +538 -0
- package/skills/data-analysis.md +27 -0
- package/skills/debugging.md +33 -0
- package/skills/devops.md +37 -0
- package/skills/document-docx.md +197 -0
- package/skills/document-pdf.md +294 -0
- package/skills/document-pptx.md +484 -0
- package/skills/document-xlsx.md +289 -0
- package/skills/domain-name-brainstormer.md +212 -0
- package/skills/file-organizer.md +433 -0
- package/skills/frontend-design.md +42 -0
- package/skills/image-enhancer.md +99 -0
- package/skills/invoice-organizer.md +446 -0
- package/skills/lead-research-assistant.md +199 -0
- package/skills/mcp-builder/SKILL.md +328 -0
- package/skills/mcp-builder/reference/evaluation.md +602 -0
- package/skills/mcp-builder/reference/mcp_best_practices.md +915 -0
- package/skills/mcp-builder/reference/node_mcp_server.md +916 -0
- package/skills/mcp-builder/reference/python_mcp_server.md +752 -0
- package/skills/mcp-builder/scripts/connections.py +151 -0
- package/skills/mcp-builder/scripts/evaluation.py +373 -0
- package/skills/mcp-builder/scripts/example_evaluation.xml +22 -0
- package/skills/mcp-builder/scripts/requirements.txt +2 -0
- package/skills/meeting-insights-analyzer.md +327 -0
- package/skills/orchestration.md +93 -0
- package/skills/raffle-winner-picker.md +159 -0
- package/skills/slack-gif-creator/SKILL.md +646 -0
- package/skills/slack-gif-creator/core/color_palettes.py +302 -0
- package/skills/slack-gif-creator/core/easing.py +230 -0
- package/skills/slack-gif-creator/core/frame_composer.py +469 -0
- package/skills/slack-gif-creator/core/gif_builder.py +246 -0
- package/skills/slack-gif-creator/core/typography.py +357 -0
- package/skills/slack-gif-creator/core/validators.py +264 -0
- package/skills/slack-gif-creator/core/visual_effects.py +494 -0
- package/skills/slack-gif-creator/requirements.txt +4 -0
- package/skills/slack-gif-creator/templates/bounce.py +106 -0
- package/skills/slack-gif-creator/templates/explode.py +331 -0
- package/skills/slack-gif-creator/templates/fade.py +329 -0
- package/skills/slack-gif-creator/templates/flip.py +291 -0
- package/skills/slack-gif-creator/templates/kaleidoscope.py +211 -0
- package/skills/slack-gif-creator/templates/morph.py +329 -0
- package/skills/slack-gif-creator/templates/move.py +293 -0
- package/skills/slack-gif-creator/templates/pulse.py +268 -0
- package/skills/slack-gif-creator/templates/shake.py +127 -0
- package/skills/slack-gif-creator/templates/slide.py +291 -0
- package/skills/slack-gif-creator/templates/spin.py +269 -0
- package/skills/slack-gif-creator/templates/wiggle.py +300 -0
- package/skills/slack-gif-creator/templates/zoom.py +312 -0
- package/skills/system-admin.md +44 -0
- package/skills/tailored-resume-generator.md +345 -0
- package/skills/theme-factory/SKILL.md +59 -0
- package/skills/theme-factory/theme-showcase.pdf +0 -0
- package/skills/theme-factory/themes/arctic-frost.md +19 -0
- package/skills/theme-factory/themes/botanical-garden.md +19 -0
- package/skills/theme-factory/themes/desert-rose.md +19 -0
- package/skills/theme-factory/themes/forest-canopy.md +19 -0
- package/skills/theme-factory/themes/golden-hour.md +19 -0
- package/skills/theme-factory/themes/midnight-galaxy.md +19 -0
- package/skills/theme-factory/themes/modern-minimalist.md +19 -0
- package/skills/theme-factory/themes/ocean-depths.md +19 -0
- package/skills/theme-factory/themes/sunset-boulevard.md +19 -0
- package/skills/theme-factory/themes/tech-innovation.md +19 -0
- package/skills/video-downloader.md +99 -0
- package/skills/web-development.md +32 -0
- package/skills/webapp-testing/SKILL.md +96 -0
- package/skills/webapp-testing/examples/console_logging.py +35 -0
- package/skills/webapp-testing/examples/element_discovery.py +40 -0
- package/skills/webapp-testing/examples/static_html_automation.py +33 -0
- package/skills/webapp-testing/scripts/with_server.py +106 -0
- package/src/agents/SubAgentManager.js +57 -12
- package/src/api/openai-compat.js +212 -0
- package/src/channels/TelegramChannel.js +5 -2
- package/src/channels/index.js +7 -10
- package/src/cli.js +129 -50
- package/src/config/agentProfiles.js +1 -0
- package/src/config/default.js +10 -0
- package/src/config/models.js +317 -71
- package/src/config/permissions.js +12 -0
- package/src/core/AgentLoop.js +70 -50
- package/src/core/Compaction.js +84 -2
- package/src/core/MessageQueue.js +90 -0
- package/src/core/Task.js +13 -0
- package/src/core/TaskQueue.js +1 -1
- package/src/core/TaskRunner.js +80 -5
- package/src/index.js +328 -48
- package/src/mcp/MCPAgentRunner.js +48 -11
- package/src/mcp/MCPManager.js +40 -2
- package/src/models/ModelRouter.js +67 -1
- package/src/safety/DockerSandbox.js +212 -0
- package/src/safety/ExecApproval.js +118 -0
- package/src/scheduler/Heartbeat.js +56 -21
- package/src/services/cleanup.js +106 -0
- package/src/services/sessions.js +39 -1
- package/src/setup/wizard.js +75 -4
- package/src/skills/SkillLoader.js +104 -17
- package/src/storage/TaskStore.js +19 -1
- package/src/systemPrompt.js +171 -328
- package/src/tools/browserAutomation.js +615 -104
- package/src/tools/executeCommand.js +19 -1
- package/src/tools/index.js +6 -0
- package/src/tools/manageAgents.js +55 -4
- package/src/tools/replyWithFile.js +62 -0
- package/src/tools/screenCapture.js +12 -1
- package/src/tools/taskManager.js +164 -0
- package/src/tools/useMCP.js +3 -1
- package/src/utils/Embeddings.js +157 -10
- package/src/webhooks/WebhookHandler.js +107 -0
|
@@ -0,0 +1,291 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""
|
|
3
|
+
Flip Animation - 3D-style card flip and rotation effects.
|
|
4
|
+
|
|
5
|
+
Creates horizontal and vertical flips with perspective.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
import sys
|
|
9
|
+
from pathlib import Path
|
|
10
|
+
import math
|
|
11
|
+
|
|
12
|
+
sys.path.append(str(Path(__file__).parent.parent))
|
|
13
|
+
|
|
14
|
+
from PIL import Image
|
|
15
|
+
from core.gif_builder import GIFBuilder
|
|
16
|
+
from core.frame_composer import create_blank_frame, draw_emoji_enhanced
|
|
17
|
+
from core.easing import interpolate
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
def create_flip_animation(
|
|
21
|
+
object1_data: dict,
|
|
22
|
+
object2_data: dict | None = None,
|
|
23
|
+
num_frames: int = 30,
|
|
24
|
+
flip_axis: str = 'horizontal', # 'horizontal', 'vertical'
|
|
25
|
+
easing: str = 'ease_in_out',
|
|
26
|
+
object_type: str = 'emoji',
|
|
27
|
+
center_pos: tuple[int, int] = (240, 240),
|
|
28
|
+
frame_width: int = 480,
|
|
29
|
+
frame_height: int = 480,
|
|
30
|
+
bg_color: tuple[int, int, int] = (255, 255, 255)
|
|
31
|
+
) -> list[Image.Image]:
|
|
32
|
+
"""
|
|
33
|
+
Create 3D-style flip animation.
|
|
34
|
+
|
|
35
|
+
Args:
|
|
36
|
+
object1_data: First object (front side)
|
|
37
|
+
object2_data: Second object (back side, None = same as front)
|
|
38
|
+
num_frames: Number of frames
|
|
39
|
+
flip_axis: Axis to flip around
|
|
40
|
+
easing: Easing function
|
|
41
|
+
object_type: Type of objects
|
|
42
|
+
center_pos: Center position
|
|
43
|
+
frame_width: Frame width
|
|
44
|
+
frame_height: Frame height
|
|
45
|
+
bg_color: Background color
|
|
46
|
+
|
|
47
|
+
Returns:
|
|
48
|
+
List of frames
|
|
49
|
+
"""
|
|
50
|
+
frames = []
|
|
51
|
+
|
|
52
|
+
if object2_data is None:
|
|
53
|
+
object2_data = object1_data
|
|
54
|
+
|
|
55
|
+
for i in range(num_frames):
|
|
56
|
+
t = i / (num_frames - 1) if num_frames > 1 else 0
|
|
57
|
+
frame = create_blank_frame(frame_width, frame_height, bg_color)
|
|
58
|
+
|
|
59
|
+
# Calculate rotation angle (0 to 180 degrees)
|
|
60
|
+
angle = interpolate(0, 180, t, easing)
|
|
61
|
+
|
|
62
|
+
# Determine which side is visible and calculate scale
|
|
63
|
+
if angle < 90:
|
|
64
|
+
# Front side visible
|
|
65
|
+
current_object = object1_data
|
|
66
|
+
scale_factor = math.cos(math.radians(angle))
|
|
67
|
+
else:
|
|
68
|
+
# Back side visible
|
|
69
|
+
current_object = object2_data
|
|
70
|
+
scale_factor = abs(math.cos(math.radians(angle)))
|
|
71
|
+
|
|
72
|
+
# Don't draw when edge-on (very thin)
|
|
73
|
+
if scale_factor < 0.05:
|
|
74
|
+
frames.append(frame)
|
|
75
|
+
continue
|
|
76
|
+
|
|
77
|
+
if object_type == 'emoji':
|
|
78
|
+
size = current_object['size']
|
|
79
|
+
|
|
80
|
+
# Create emoji on canvas
|
|
81
|
+
canvas_size = size * 2
|
|
82
|
+
emoji_canvas = Image.new('RGBA', (canvas_size, canvas_size), (0, 0, 0, 0))
|
|
83
|
+
|
|
84
|
+
draw_emoji_enhanced(
|
|
85
|
+
emoji_canvas,
|
|
86
|
+
emoji=current_object['emoji'],
|
|
87
|
+
position=(canvas_size // 2 - size // 2, canvas_size // 2 - size // 2),
|
|
88
|
+
size=size,
|
|
89
|
+
shadow=False
|
|
90
|
+
)
|
|
91
|
+
|
|
92
|
+
# Apply flip scaling
|
|
93
|
+
if flip_axis == 'horizontal':
|
|
94
|
+
# Scale horizontally for horizontal flip
|
|
95
|
+
new_width = max(1, int(canvas_size * scale_factor))
|
|
96
|
+
new_height = canvas_size
|
|
97
|
+
else:
|
|
98
|
+
# Scale vertically for vertical flip
|
|
99
|
+
new_width = canvas_size
|
|
100
|
+
new_height = max(1, int(canvas_size * scale_factor))
|
|
101
|
+
|
|
102
|
+
# Resize to simulate 3D rotation
|
|
103
|
+
emoji_scaled = emoji_canvas.resize((new_width, new_height), Image.LANCZOS)
|
|
104
|
+
|
|
105
|
+
# Position centered
|
|
106
|
+
paste_x = center_pos[0] - new_width // 2
|
|
107
|
+
paste_y = center_pos[1] - new_height // 2
|
|
108
|
+
|
|
109
|
+
# Composite onto frame
|
|
110
|
+
frame_rgba = frame.convert('RGBA')
|
|
111
|
+
frame_rgba.paste(emoji_scaled, (paste_x, paste_y), emoji_scaled)
|
|
112
|
+
frame = frame_rgba.convert('RGB')
|
|
113
|
+
|
|
114
|
+
elif object_type == 'text':
|
|
115
|
+
from core.typography import draw_text_with_outline
|
|
116
|
+
|
|
117
|
+
# Create text on canvas
|
|
118
|
+
text = current_object.get('text', 'FLIP')
|
|
119
|
+
font_size = current_object.get('font_size', 50)
|
|
120
|
+
|
|
121
|
+
canvas_size = max(frame_width, frame_height)
|
|
122
|
+
text_canvas = Image.new('RGBA', (canvas_size, canvas_size), (0, 0, 0, 0))
|
|
123
|
+
|
|
124
|
+
# Draw on RGB for text rendering
|
|
125
|
+
text_canvas_rgb = text_canvas.convert('RGB')
|
|
126
|
+
text_canvas_rgb.paste(bg_color, (0, 0, canvas_size, canvas_size))
|
|
127
|
+
|
|
128
|
+
draw_text_with_outline(
|
|
129
|
+
text_canvas_rgb,
|
|
130
|
+
text=text,
|
|
131
|
+
position=(canvas_size // 2, canvas_size // 2),
|
|
132
|
+
font_size=font_size,
|
|
133
|
+
text_color=current_object.get('text_color', (0, 0, 0)),
|
|
134
|
+
outline_color=current_object.get('outline_color', (255, 255, 255)),
|
|
135
|
+
outline_width=3,
|
|
136
|
+
centered=True
|
|
137
|
+
)
|
|
138
|
+
|
|
139
|
+
# Make background transparent
|
|
140
|
+
text_canvas = text_canvas_rgb.convert('RGBA')
|
|
141
|
+
data = text_canvas.getdata()
|
|
142
|
+
new_data = []
|
|
143
|
+
for item in data:
|
|
144
|
+
if item[:3] == bg_color:
|
|
145
|
+
new_data.append((255, 255, 255, 0))
|
|
146
|
+
else:
|
|
147
|
+
new_data.append(item)
|
|
148
|
+
text_canvas.putdata(new_data)
|
|
149
|
+
|
|
150
|
+
# Apply flip scaling
|
|
151
|
+
if flip_axis == 'horizontal':
|
|
152
|
+
new_width = max(1, int(canvas_size * scale_factor))
|
|
153
|
+
new_height = canvas_size
|
|
154
|
+
else:
|
|
155
|
+
new_width = canvas_size
|
|
156
|
+
new_height = max(1, int(canvas_size * scale_factor))
|
|
157
|
+
|
|
158
|
+
text_scaled = text_canvas.resize((new_width, new_height), Image.LANCZOS)
|
|
159
|
+
|
|
160
|
+
# Center and crop
|
|
161
|
+
if flip_axis == 'horizontal':
|
|
162
|
+
left = (new_width - frame_width) // 2 if new_width > frame_width else 0
|
|
163
|
+
top = (canvas_size - frame_height) // 2
|
|
164
|
+
paste_x = center_pos[0] - min(new_width, frame_width) // 2
|
|
165
|
+
paste_y = 0
|
|
166
|
+
|
|
167
|
+
text_cropped = text_scaled.crop((
|
|
168
|
+
left,
|
|
169
|
+
top,
|
|
170
|
+
left + min(new_width, frame_width),
|
|
171
|
+
top + frame_height
|
|
172
|
+
))
|
|
173
|
+
else:
|
|
174
|
+
left = (canvas_size - frame_width) // 2
|
|
175
|
+
top = (new_height - frame_height) // 2 if new_height > frame_height else 0
|
|
176
|
+
paste_x = 0
|
|
177
|
+
paste_y = center_pos[1] - min(new_height, frame_height) // 2
|
|
178
|
+
|
|
179
|
+
text_cropped = text_scaled.crop((
|
|
180
|
+
left,
|
|
181
|
+
top,
|
|
182
|
+
left + frame_width,
|
|
183
|
+
top + min(new_height, frame_height)
|
|
184
|
+
))
|
|
185
|
+
|
|
186
|
+
frame_rgba = frame.convert('RGBA')
|
|
187
|
+
frame_rgba.paste(text_cropped, (paste_x, paste_y), text_cropped)
|
|
188
|
+
frame = frame_rgba.convert('RGB')
|
|
189
|
+
|
|
190
|
+
frames.append(frame)
|
|
191
|
+
|
|
192
|
+
return frames
|
|
193
|
+
|
|
194
|
+
|
|
195
|
+
def create_quick_flip(
|
|
196
|
+
emoji_front: str,
|
|
197
|
+
emoji_back: str,
|
|
198
|
+
num_frames: int = 20,
|
|
199
|
+
frame_size: int = 128
|
|
200
|
+
) -> list[Image.Image]:
|
|
201
|
+
"""
|
|
202
|
+
Create quick flip for emoji GIFs.
|
|
203
|
+
|
|
204
|
+
Args:
|
|
205
|
+
emoji_front: Front emoji
|
|
206
|
+
emoji_back: Back emoji
|
|
207
|
+
num_frames: Number of frames
|
|
208
|
+
frame_size: Frame size (square)
|
|
209
|
+
|
|
210
|
+
Returns:
|
|
211
|
+
List of frames
|
|
212
|
+
"""
|
|
213
|
+
return create_flip_animation(
|
|
214
|
+
object1_data={'emoji': emoji_front, 'size': 80},
|
|
215
|
+
object2_data={'emoji': emoji_back, 'size': 80},
|
|
216
|
+
num_frames=num_frames,
|
|
217
|
+
flip_axis='horizontal',
|
|
218
|
+
easing='ease_in_out',
|
|
219
|
+
object_type='emoji',
|
|
220
|
+
center_pos=(frame_size // 2, frame_size // 2),
|
|
221
|
+
frame_width=frame_size,
|
|
222
|
+
frame_height=frame_size,
|
|
223
|
+
bg_color=(255, 255, 255)
|
|
224
|
+
)
|
|
225
|
+
|
|
226
|
+
|
|
227
|
+
def create_nope_flip(
|
|
228
|
+
num_frames: int = 25,
|
|
229
|
+
frame_width: int = 480,
|
|
230
|
+
frame_height: int = 480
|
|
231
|
+
) -> list[Image.Image]:
|
|
232
|
+
"""
|
|
233
|
+
Create "nope" reaction flip (like flipping table).
|
|
234
|
+
|
|
235
|
+
Args:
|
|
236
|
+
num_frames: Number of frames
|
|
237
|
+
frame_width: Frame width
|
|
238
|
+
frame_height: Frame height
|
|
239
|
+
|
|
240
|
+
Returns:
|
|
241
|
+
List of frames
|
|
242
|
+
"""
|
|
243
|
+
return create_flip_animation(
|
|
244
|
+
object1_data={'text': 'NOPE', 'font_size': 80, 'text_color': (255, 50, 50)},
|
|
245
|
+
object2_data={'text': 'NOPE', 'font_size': 80, 'text_color': (255, 50, 50)},
|
|
246
|
+
num_frames=num_frames,
|
|
247
|
+
flip_axis='horizontal',
|
|
248
|
+
easing='ease_out',
|
|
249
|
+
object_type='text',
|
|
250
|
+
frame_width=frame_width,
|
|
251
|
+
frame_height=frame_height,
|
|
252
|
+
bg_color=(255, 255, 255)
|
|
253
|
+
)
|
|
254
|
+
|
|
255
|
+
|
|
256
|
+
# Example usage
|
|
257
|
+
if __name__ == '__main__':
|
|
258
|
+
print("Creating flip animations...")
|
|
259
|
+
|
|
260
|
+
builder = GIFBuilder(width=480, height=480, fps=20)
|
|
261
|
+
|
|
262
|
+
# Example 1: Emoji flip
|
|
263
|
+
frames = create_flip_animation(
|
|
264
|
+
object1_data={'emoji': '😊', 'size': 120},
|
|
265
|
+
object2_data={'emoji': '😂', 'size': 120},
|
|
266
|
+
num_frames=30,
|
|
267
|
+
flip_axis='horizontal',
|
|
268
|
+
object_type='emoji'
|
|
269
|
+
)
|
|
270
|
+
builder.add_frames(frames)
|
|
271
|
+
builder.save('flip_emoji.gif', num_colors=128)
|
|
272
|
+
|
|
273
|
+
# Example 2: Text flip
|
|
274
|
+
builder.clear()
|
|
275
|
+
frames = create_flip_animation(
|
|
276
|
+
object1_data={'text': 'YES', 'font_size': 80, 'text_color': (100, 200, 100)},
|
|
277
|
+
object2_data={'text': 'NO', 'font_size': 80, 'text_color': (200, 100, 100)},
|
|
278
|
+
num_frames=30,
|
|
279
|
+
flip_axis='vertical',
|
|
280
|
+
object_type='text'
|
|
281
|
+
)
|
|
282
|
+
builder.add_frames(frames)
|
|
283
|
+
builder.save('flip_text.gif', num_colors=128)
|
|
284
|
+
|
|
285
|
+
# Example 3: Quick flip (emoji size)
|
|
286
|
+
builder = GIFBuilder(width=128, height=128, fps=15)
|
|
287
|
+
frames = create_quick_flip('👍', '👎', num_frames=20)
|
|
288
|
+
builder.add_frames(frames)
|
|
289
|
+
builder.save('flip_quick.gif', num_colors=48, optimize_for_emoji=True)
|
|
290
|
+
|
|
291
|
+
print("Created flip animations!")
|
|
@@ -0,0 +1,211 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""
|
|
3
|
+
Kaleidoscope Effect - Create mirror/rotation effects.
|
|
4
|
+
|
|
5
|
+
Apply kaleidoscope effects to frames or objects for psychedelic visuals.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
import sys
|
|
9
|
+
from pathlib import Path
|
|
10
|
+
import math
|
|
11
|
+
|
|
12
|
+
sys.path.append(str(Path(__file__).parent.parent))
|
|
13
|
+
|
|
14
|
+
from PIL import Image, ImageOps, ImageDraw
|
|
15
|
+
import numpy as np
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
def 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
|
+
|
|
86
|
+
def 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
|
+
|
|
140
|
+
def 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
|
|
196
|
+
if __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)
|