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,269 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""
|
|
3
|
+
Spin Animation - Rotate objects continuously or with variation.
|
|
4
|
+
|
|
5
|
+
Creates spinning, rotating, and wobbling effects.
|
|
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, draw_circle
|
|
17
|
+
from core.easing import interpolate
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
def create_spin_animation(
|
|
21
|
+
object_type: str = 'emoji',
|
|
22
|
+
object_data: dict | None = None,
|
|
23
|
+
num_frames: int = 30,
|
|
24
|
+
rotation_type: str = 'clockwise', # 'clockwise', 'counterclockwise', 'wobble', 'pendulum'
|
|
25
|
+
full_rotations: float = 1.0,
|
|
26
|
+
easing: str = 'linear',
|
|
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 spinning/rotating animation.
|
|
34
|
+
|
|
35
|
+
Args:
|
|
36
|
+
object_type: 'emoji', 'image', 'text'
|
|
37
|
+
object_data: Object configuration
|
|
38
|
+
num_frames: Number of frames
|
|
39
|
+
rotation_type: Type of rotation
|
|
40
|
+
full_rotations: Number of complete 360° rotations
|
|
41
|
+
easing: Easing function for rotation speed
|
|
42
|
+
center_pos: Center position for rotation
|
|
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
|
+
# Default object data
|
|
53
|
+
if object_data is None:
|
|
54
|
+
if object_type == 'emoji':
|
|
55
|
+
object_data = {'emoji': '🔄', 'size': 100}
|
|
56
|
+
|
|
57
|
+
for i in range(num_frames):
|
|
58
|
+
frame = create_blank_frame(frame_width, frame_height, bg_color)
|
|
59
|
+
t = i / (num_frames - 1) if num_frames > 1 else 0
|
|
60
|
+
|
|
61
|
+
# Calculate rotation angle
|
|
62
|
+
if rotation_type == 'clockwise':
|
|
63
|
+
angle = interpolate(0, 360 * full_rotations, t, easing)
|
|
64
|
+
elif rotation_type == 'counterclockwise':
|
|
65
|
+
angle = interpolate(0, -360 * full_rotations, t, easing)
|
|
66
|
+
elif rotation_type == 'wobble':
|
|
67
|
+
# Back and forth rotation
|
|
68
|
+
angle = math.sin(t * full_rotations * 2 * math.pi) * 45
|
|
69
|
+
elif rotation_type == 'pendulum':
|
|
70
|
+
# Smooth pendulum swing
|
|
71
|
+
angle = math.sin(t * full_rotations * 2 * math.pi) * 90
|
|
72
|
+
else:
|
|
73
|
+
angle = interpolate(0, 360 * full_rotations, t, easing)
|
|
74
|
+
|
|
75
|
+
# Create object on transparent background to rotate
|
|
76
|
+
if object_type == 'emoji':
|
|
77
|
+
# For emoji, we need to create a larger canvas to avoid clipping during rotation
|
|
78
|
+
emoji_size = object_data['size']
|
|
79
|
+
canvas_size = int(emoji_size * 1.5)
|
|
80
|
+
emoji_canvas = Image.new('RGBA', (canvas_size, canvas_size), (0, 0, 0, 0))
|
|
81
|
+
|
|
82
|
+
# Draw emoji in center of canvas
|
|
83
|
+
from core.frame_composer import draw_emoji_enhanced
|
|
84
|
+
draw_emoji_enhanced(
|
|
85
|
+
emoji_canvas,
|
|
86
|
+
emoji=object_data['emoji'],
|
|
87
|
+
position=(canvas_size // 2 - emoji_size // 2, canvas_size // 2 - emoji_size // 2),
|
|
88
|
+
size=emoji_size,
|
|
89
|
+
shadow=False
|
|
90
|
+
)
|
|
91
|
+
|
|
92
|
+
# Rotate the canvas
|
|
93
|
+
rotated = emoji_canvas.rotate(angle, resample=Image.BICUBIC, expand=False)
|
|
94
|
+
|
|
95
|
+
# Paste onto frame
|
|
96
|
+
paste_x = center_pos[0] - canvas_size // 2
|
|
97
|
+
paste_y = center_pos[1] - canvas_size // 2
|
|
98
|
+
frame.paste(rotated, (paste_x, paste_y), rotated)
|
|
99
|
+
|
|
100
|
+
elif object_type == 'text':
|
|
101
|
+
from core.typography import draw_text_with_outline
|
|
102
|
+
# Similar approach - create canvas, draw text, rotate
|
|
103
|
+
text = object_data.get('text', 'SPIN!')
|
|
104
|
+
font_size = object_data.get('font_size', 50)
|
|
105
|
+
|
|
106
|
+
canvas_size = max(frame_width, frame_height)
|
|
107
|
+
text_canvas = Image.new('RGBA', (canvas_size, canvas_size), (0, 0, 0, 0))
|
|
108
|
+
|
|
109
|
+
# Draw text
|
|
110
|
+
text_canvas_rgb = text_canvas.convert('RGB')
|
|
111
|
+
text_canvas_rgb.paste(bg_color, (0, 0, canvas_size, canvas_size))
|
|
112
|
+
draw_text_with_outline(
|
|
113
|
+
text_canvas_rgb,
|
|
114
|
+
text,
|
|
115
|
+
position=(canvas_size // 2, canvas_size // 2),
|
|
116
|
+
font_size=font_size,
|
|
117
|
+
text_color=object_data.get('text_color', (0, 0, 0)),
|
|
118
|
+
outline_color=object_data.get('outline_color', (255, 255, 255)),
|
|
119
|
+
outline_width=3,
|
|
120
|
+
centered=True
|
|
121
|
+
)
|
|
122
|
+
|
|
123
|
+
# Convert back to RGBA for rotation
|
|
124
|
+
text_canvas = text_canvas_rgb.convert('RGBA')
|
|
125
|
+
|
|
126
|
+
# Make background transparent
|
|
127
|
+
data = text_canvas.getdata()
|
|
128
|
+
new_data = []
|
|
129
|
+
for item in data:
|
|
130
|
+
if item[:3] == bg_color:
|
|
131
|
+
new_data.append((255, 255, 255, 0))
|
|
132
|
+
else:
|
|
133
|
+
new_data.append(item)
|
|
134
|
+
text_canvas.putdata(new_data)
|
|
135
|
+
|
|
136
|
+
# Rotate
|
|
137
|
+
rotated = text_canvas.rotate(angle, resample=Image.BICUBIC, expand=False)
|
|
138
|
+
|
|
139
|
+
# Composite onto frame
|
|
140
|
+
frame_rgba = frame.convert('RGBA')
|
|
141
|
+
frame_rgba = Image.alpha_composite(frame_rgba, rotated)
|
|
142
|
+
frame = frame_rgba.convert('RGB')
|
|
143
|
+
|
|
144
|
+
frames.append(frame)
|
|
145
|
+
|
|
146
|
+
return frames
|
|
147
|
+
|
|
148
|
+
|
|
149
|
+
def create_loading_spinner(
|
|
150
|
+
num_frames: int = 20,
|
|
151
|
+
spinner_type: str = 'dots', # 'dots', 'arc', 'emoji'
|
|
152
|
+
size: int = 100,
|
|
153
|
+
color: tuple[int, int, int] = (100, 150, 255),
|
|
154
|
+
frame_width: int = 128,
|
|
155
|
+
frame_height: int = 128,
|
|
156
|
+
bg_color: tuple[int, int, int] = (255, 255, 255)
|
|
157
|
+
) -> list[Image.Image]:
|
|
158
|
+
"""
|
|
159
|
+
Create a loading spinner animation.
|
|
160
|
+
|
|
161
|
+
Args:
|
|
162
|
+
num_frames: Number of frames
|
|
163
|
+
spinner_type: Type of spinner
|
|
164
|
+
size: Spinner size
|
|
165
|
+
color: Spinner color
|
|
166
|
+
frame_width: Frame width
|
|
167
|
+
frame_height: Frame height
|
|
168
|
+
bg_color: Background color
|
|
169
|
+
|
|
170
|
+
Returns:
|
|
171
|
+
List of frames
|
|
172
|
+
"""
|
|
173
|
+
from PIL import ImageDraw
|
|
174
|
+
frames = []
|
|
175
|
+
center = (frame_width // 2, frame_height // 2)
|
|
176
|
+
|
|
177
|
+
for i in range(num_frames):
|
|
178
|
+
frame = create_blank_frame(frame_width, frame_height, bg_color)
|
|
179
|
+
draw = ImageDraw.Draw(frame)
|
|
180
|
+
|
|
181
|
+
angle_offset = (i / num_frames) * 360
|
|
182
|
+
|
|
183
|
+
if spinner_type == 'dots':
|
|
184
|
+
# Circular dots
|
|
185
|
+
num_dots = 8
|
|
186
|
+
for j in range(num_dots):
|
|
187
|
+
angle = (j / num_dots * 360 + angle_offset) * math.pi / 180
|
|
188
|
+
x = center[0] + size * 0.4 * math.cos(angle)
|
|
189
|
+
y = center[1] + size * 0.4 * math.sin(angle)
|
|
190
|
+
|
|
191
|
+
# Fade based on position
|
|
192
|
+
alpha = 1.0 - (j / num_dots)
|
|
193
|
+
dot_color = tuple(int(c * alpha) for c in color)
|
|
194
|
+
dot_radius = int(size * 0.1)
|
|
195
|
+
|
|
196
|
+
draw.ellipse(
|
|
197
|
+
[x - dot_radius, y - dot_radius, x + dot_radius, y + dot_radius],
|
|
198
|
+
fill=dot_color
|
|
199
|
+
)
|
|
200
|
+
|
|
201
|
+
elif spinner_type == 'arc':
|
|
202
|
+
# Rotating arc
|
|
203
|
+
start_angle = angle_offset
|
|
204
|
+
end_angle = angle_offset + 270
|
|
205
|
+
arc_width = int(size * 0.15)
|
|
206
|
+
|
|
207
|
+
bbox = [
|
|
208
|
+
center[0] - size // 2,
|
|
209
|
+
center[1] - size // 2,
|
|
210
|
+
center[0] + size // 2,
|
|
211
|
+
center[1] + size // 2
|
|
212
|
+
]
|
|
213
|
+
draw.arc(bbox, start_angle, end_angle, fill=color, width=arc_width)
|
|
214
|
+
|
|
215
|
+
elif spinner_type == 'emoji':
|
|
216
|
+
# Rotating emoji spinner
|
|
217
|
+
angle = angle_offset
|
|
218
|
+
emoji_canvas = Image.new('RGBA', (frame_width, frame_height), (0, 0, 0, 0))
|
|
219
|
+
draw_emoji_enhanced(
|
|
220
|
+
emoji_canvas,
|
|
221
|
+
emoji='⏳',
|
|
222
|
+
position=(center[0] - size // 2, center[1] - size // 2),
|
|
223
|
+
size=size,
|
|
224
|
+
shadow=False
|
|
225
|
+
)
|
|
226
|
+
rotated = emoji_canvas.rotate(angle, center=center, resample=Image.BICUBIC)
|
|
227
|
+
frame.paste(rotated, (0, 0), rotated)
|
|
228
|
+
|
|
229
|
+
frames.append(frame)
|
|
230
|
+
|
|
231
|
+
return frames
|
|
232
|
+
|
|
233
|
+
|
|
234
|
+
# Example usage
|
|
235
|
+
if __name__ == '__main__':
|
|
236
|
+
print("Creating spin animations...")
|
|
237
|
+
|
|
238
|
+
builder = GIFBuilder(width=480, height=480, fps=20)
|
|
239
|
+
|
|
240
|
+
# Example 1: Clockwise spin
|
|
241
|
+
frames = create_spin_animation(
|
|
242
|
+
object_type='emoji',
|
|
243
|
+
object_data={'emoji': '🔄', 'size': 100},
|
|
244
|
+
num_frames=30,
|
|
245
|
+
rotation_type='clockwise',
|
|
246
|
+
full_rotations=2
|
|
247
|
+
)
|
|
248
|
+
builder.add_frames(frames)
|
|
249
|
+
builder.save('spin_clockwise.gif', num_colors=128)
|
|
250
|
+
|
|
251
|
+
# Example 2: Wobble
|
|
252
|
+
builder.clear()
|
|
253
|
+
frames = create_spin_animation(
|
|
254
|
+
object_type='emoji',
|
|
255
|
+
object_data={'emoji': '🎯', 'size': 100},
|
|
256
|
+
num_frames=30,
|
|
257
|
+
rotation_type='wobble',
|
|
258
|
+
full_rotations=3
|
|
259
|
+
)
|
|
260
|
+
builder.add_frames(frames)
|
|
261
|
+
builder.save('spin_wobble.gif', num_colors=128)
|
|
262
|
+
|
|
263
|
+
# Example 3: Loading spinner
|
|
264
|
+
builder = GIFBuilder(width=128, height=128, fps=15)
|
|
265
|
+
frames = create_loading_spinner(num_frames=20, spinner_type='dots')
|
|
266
|
+
builder.add_frames(frames)
|
|
267
|
+
builder.save('loading_spinner.gif', num_colors=64, optimize_for_emoji=True)
|
|
268
|
+
|
|
269
|
+
print("Created spin animations!")
|
|
@@ -0,0 +1,300 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""
|
|
3
|
+
Wiggle Animation - Smooth, organic wobbling and jiggling motions.
|
|
4
|
+
|
|
5
|
+
Creates playful, elastic movements that are smoother than shake.
|
|
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_wiggle_animation(
|
|
21
|
+
object_type: str = 'emoji',
|
|
22
|
+
object_data: dict | None = None,
|
|
23
|
+
num_frames: int = 30,
|
|
24
|
+
wiggle_type: str = 'jello', # 'jello', 'wave', 'bounce', 'sway'
|
|
25
|
+
intensity: float = 1.0,
|
|
26
|
+
cycles: float = 2.0,
|
|
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 wiggle/wobble animation.
|
|
34
|
+
|
|
35
|
+
Args:
|
|
36
|
+
object_type: 'emoji', 'text'
|
|
37
|
+
object_data: Object configuration
|
|
38
|
+
num_frames: Number of frames
|
|
39
|
+
wiggle_type: Type of wiggle motion
|
|
40
|
+
intensity: Wiggle intensity multiplier
|
|
41
|
+
cycles: Number of wiggle cycles
|
|
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
|
+
# Default object data
|
|
53
|
+
if object_data is None:
|
|
54
|
+
if object_type == 'emoji':
|
|
55
|
+
object_data = {'emoji': '🎈', 'size': 100}
|
|
56
|
+
|
|
57
|
+
for i in range(num_frames):
|
|
58
|
+
t = i / (num_frames - 1) if num_frames > 1 else 0
|
|
59
|
+
frame = create_blank_frame(frame_width, frame_height, bg_color)
|
|
60
|
+
|
|
61
|
+
# Calculate wiggle transformations
|
|
62
|
+
offset_x = 0
|
|
63
|
+
offset_y = 0
|
|
64
|
+
rotation = 0
|
|
65
|
+
scale_x = 1.0
|
|
66
|
+
scale_y = 1.0
|
|
67
|
+
|
|
68
|
+
if wiggle_type == 'jello':
|
|
69
|
+
# Jello wobble - multiple frequencies
|
|
70
|
+
freq1 = cycles * 2 * math.pi
|
|
71
|
+
freq2 = cycles * 3 * math.pi
|
|
72
|
+
freq3 = cycles * 5 * math.pi
|
|
73
|
+
|
|
74
|
+
decay = 1.0 - t if cycles < 1.5 else 1.0 # Decay for single wiggles
|
|
75
|
+
|
|
76
|
+
offset_x = (
|
|
77
|
+
math.sin(freq1 * t) * 15 +
|
|
78
|
+
math.sin(freq2 * t) * 8 +
|
|
79
|
+
math.sin(freq3 * t) * 3
|
|
80
|
+
) * intensity * decay
|
|
81
|
+
|
|
82
|
+
rotation = (
|
|
83
|
+
math.sin(freq1 * t) * 10 +
|
|
84
|
+
math.cos(freq2 * t) * 5
|
|
85
|
+
) * intensity * decay
|
|
86
|
+
|
|
87
|
+
# Squash and stretch
|
|
88
|
+
scale_y = 1.0 + math.sin(freq1 * t) * 0.1 * intensity * decay
|
|
89
|
+
scale_x = 1.0 / scale_y # Preserve volume
|
|
90
|
+
|
|
91
|
+
elif wiggle_type == 'wave':
|
|
92
|
+
# Wave motion
|
|
93
|
+
freq = cycles * 2 * math.pi
|
|
94
|
+
offset_y = math.sin(freq * t) * 20 * intensity
|
|
95
|
+
rotation = math.sin(freq * t + math.pi / 4) * 8 * intensity
|
|
96
|
+
|
|
97
|
+
elif wiggle_type == 'bounce':
|
|
98
|
+
# Bouncy wiggle
|
|
99
|
+
freq = cycles * 2 * math.pi
|
|
100
|
+
bounce = abs(math.sin(freq * t))
|
|
101
|
+
|
|
102
|
+
scale_y = 1.0 + bounce * 0.2 * intensity
|
|
103
|
+
scale_x = 1.0 - bounce * 0.1 * intensity
|
|
104
|
+
offset_y = -bounce * 10 * intensity
|
|
105
|
+
|
|
106
|
+
elif wiggle_type == 'sway':
|
|
107
|
+
# Gentle sway back and forth
|
|
108
|
+
freq = cycles * 2 * math.pi
|
|
109
|
+
offset_x = math.sin(freq * t) * 25 * intensity
|
|
110
|
+
rotation = math.sin(freq * t) * 12 * intensity
|
|
111
|
+
|
|
112
|
+
# Subtle scale change
|
|
113
|
+
scale = 1.0 + math.sin(freq * t) * 0.05 * intensity
|
|
114
|
+
scale_x = scale
|
|
115
|
+
scale_y = scale
|
|
116
|
+
|
|
117
|
+
elif wiggle_type == 'tail_wag':
|
|
118
|
+
# Like a wagging tail - base stays, tip moves
|
|
119
|
+
freq = cycles * 2 * math.pi
|
|
120
|
+
wag = math.sin(freq * t) * intensity
|
|
121
|
+
|
|
122
|
+
# Rotation focused at one end
|
|
123
|
+
rotation = wag * 20
|
|
124
|
+
offset_x = wag * 15
|
|
125
|
+
|
|
126
|
+
# Apply transformations
|
|
127
|
+
if object_type == 'emoji':
|
|
128
|
+
size = object_data['size']
|
|
129
|
+
size_x = int(size * scale_x)
|
|
130
|
+
size_y = int(size * scale_y)
|
|
131
|
+
|
|
132
|
+
# For non-uniform scaling or rotation, we need to use PIL transforms
|
|
133
|
+
if abs(scale_x - scale_y) > 0.01 or abs(rotation) > 0.1:
|
|
134
|
+
# Create emoji on transparent canvas
|
|
135
|
+
canvas_size = int(size * 2)
|
|
136
|
+
emoji_canvas = Image.new('RGBA', (canvas_size, canvas_size), (0, 0, 0, 0))
|
|
137
|
+
|
|
138
|
+
# Draw emoji
|
|
139
|
+
draw_emoji_enhanced(
|
|
140
|
+
emoji_canvas,
|
|
141
|
+
emoji=object_data['emoji'],
|
|
142
|
+
position=(canvas_size // 2 - size // 2, canvas_size // 2 - size // 2),
|
|
143
|
+
size=size,
|
|
144
|
+
shadow=False
|
|
145
|
+
)
|
|
146
|
+
|
|
147
|
+
# Scale
|
|
148
|
+
if abs(scale_x - scale_y) > 0.01:
|
|
149
|
+
new_size = (int(canvas_size * scale_x), int(canvas_size * scale_y))
|
|
150
|
+
emoji_canvas = emoji_canvas.resize(new_size, Image.LANCZOS)
|
|
151
|
+
canvas_size_x, canvas_size_y = new_size
|
|
152
|
+
else:
|
|
153
|
+
canvas_size_x = canvas_size_y = canvas_size
|
|
154
|
+
|
|
155
|
+
# Rotate
|
|
156
|
+
if abs(rotation) > 0.1:
|
|
157
|
+
emoji_canvas = emoji_canvas.rotate(
|
|
158
|
+
rotation,
|
|
159
|
+
resample=Image.BICUBIC,
|
|
160
|
+
expand=False
|
|
161
|
+
)
|
|
162
|
+
|
|
163
|
+
# Position with offset
|
|
164
|
+
paste_x = int(center_pos[0] - canvas_size_x // 2 + offset_x)
|
|
165
|
+
paste_y = int(center_pos[1] - canvas_size_y // 2 + offset_y)
|
|
166
|
+
|
|
167
|
+
frame_rgba = frame.convert('RGBA')
|
|
168
|
+
frame_rgba.paste(emoji_canvas, (paste_x, paste_y), emoji_canvas)
|
|
169
|
+
frame = frame_rgba.convert('RGB')
|
|
170
|
+
else:
|
|
171
|
+
# Simple case - just offset
|
|
172
|
+
pos_x = int(center_pos[0] - size // 2 + offset_x)
|
|
173
|
+
pos_y = int(center_pos[1] - size // 2 + offset_y)
|
|
174
|
+
draw_emoji_enhanced(
|
|
175
|
+
frame,
|
|
176
|
+
emoji=object_data['emoji'],
|
|
177
|
+
position=(pos_x, pos_y),
|
|
178
|
+
size=size,
|
|
179
|
+
shadow=object_data.get('shadow', True)
|
|
180
|
+
)
|
|
181
|
+
|
|
182
|
+
elif object_type == 'text':
|
|
183
|
+
from core.typography import draw_text_with_outline
|
|
184
|
+
|
|
185
|
+
# Create text on canvas for transformation
|
|
186
|
+
canvas_size = max(frame_width, frame_height)
|
|
187
|
+
text_canvas = Image.new('RGBA', (canvas_size, canvas_size), (0, 0, 0, 0))
|
|
188
|
+
|
|
189
|
+
# Convert to RGB for drawing
|
|
190
|
+
text_canvas_rgb = text_canvas.convert('RGB')
|
|
191
|
+
text_canvas_rgb.paste(bg_color, (0, 0, canvas_size, canvas_size))
|
|
192
|
+
|
|
193
|
+
draw_text_with_outline(
|
|
194
|
+
text_canvas_rgb,
|
|
195
|
+
text=object_data.get('text', 'WIGGLE'),
|
|
196
|
+
position=(canvas_size // 2, canvas_size // 2),
|
|
197
|
+
font_size=object_data.get('font_size', 50),
|
|
198
|
+
text_color=object_data.get('text_color', (0, 0, 0)),
|
|
199
|
+
outline_color=object_data.get('outline_color', (255, 255, 255)),
|
|
200
|
+
outline_width=3,
|
|
201
|
+
centered=True
|
|
202
|
+
)
|
|
203
|
+
|
|
204
|
+
# Make transparent
|
|
205
|
+
text_canvas = text_canvas_rgb.convert('RGBA')
|
|
206
|
+
data = text_canvas.getdata()
|
|
207
|
+
new_data = []
|
|
208
|
+
for item in data:
|
|
209
|
+
if item[:3] == bg_color:
|
|
210
|
+
new_data.append((255, 255, 255, 0))
|
|
211
|
+
else:
|
|
212
|
+
new_data.append(item)
|
|
213
|
+
text_canvas.putdata(new_data)
|
|
214
|
+
|
|
215
|
+
# Apply rotation
|
|
216
|
+
if abs(rotation) > 0.1:
|
|
217
|
+
text_canvas = text_canvas.rotate(rotation, center=(canvas_size // 2, canvas_size // 2), resample=Image.BICUBIC)
|
|
218
|
+
|
|
219
|
+
# Crop to frame with offset
|
|
220
|
+
left = (canvas_size - frame_width) // 2 - int(offset_x)
|
|
221
|
+
top = (canvas_size - frame_height) // 2 - int(offset_y)
|
|
222
|
+
text_cropped = text_canvas.crop((left, top, left + frame_width, top + frame_height))
|
|
223
|
+
|
|
224
|
+
frame_rgba = frame.convert('RGBA')
|
|
225
|
+
frame = Image.alpha_composite(frame_rgba, text_cropped)
|
|
226
|
+
frame = frame.convert('RGB')
|
|
227
|
+
|
|
228
|
+
frames.append(frame)
|
|
229
|
+
|
|
230
|
+
return frames
|
|
231
|
+
|
|
232
|
+
|
|
233
|
+
def create_excited_wiggle(
|
|
234
|
+
emoji: str = '🎉',
|
|
235
|
+
num_frames: int = 20,
|
|
236
|
+
frame_size: int = 128
|
|
237
|
+
) -> list[Image.Image]:
|
|
238
|
+
"""
|
|
239
|
+
Create excited wiggle for emoji GIFs.
|
|
240
|
+
|
|
241
|
+
Args:
|
|
242
|
+
emoji: Emoji to wiggle
|
|
243
|
+
num_frames: Number of frames
|
|
244
|
+
frame_size: Frame size (square)
|
|
245
|
+
|
|
246
|
+
Returns:
|
|
247
|
+
List of frames
|
|
248
|
+
"""
|
|
249
|
+
return create_wiggle_animation(
|
|
250
|
+
object_type='emoji',
|
|
251
|
+
object_data={'emoji': emoji, 'size': 80, 'shadow': False},
|
|
252
|
+
num_frames=num_frames,
|
|
253
|
+
wiggle_type='jello',
|
|
254
|
+
intensity=0.8,
|
|
255
|
+
cycles=2,
|
|
256
|
+
center_pos=(frame_size // 2, frame_size // 2),
|
|
257
|
+
frame_width=frame_size,
|
|
258
|
+
frame_height=frame_size,
|
|
259
|
+
bg_color=(255, 255, 255)
|
|
260
|
+
)
|
|
261
|
+
|
|
262
|
+
|
|
263
|
+
# Example usage
|
|
264
|
+
if __name__ == '__main__':
|
|
265
|
+
print("Creating wiggle animations...")
|
|
266
|
+
|
|
267
|
+
builder = GIFBuilder(width=480, height=480, fps=20)
|
|
268
|
+
|
|
269
|
+
# Example 1: Jello wiggle
|
|
270
|
+
frames = create_wiggle_animation(
|
|
271
|
+
object_type='emoji',
|
|
272
|
+
object_data={'emoji': '🎈', 'size': 100},
|
|
273
|
+
num_frames=40,
|
|
274
|
+
wiggle_type='jello',
|
|
275
|
+
intensity=1.0,
|
|
276
|
+
cycles=2
|
|
277
|
+
)
|
|
278
|
+
builder.add_frames(frames)
|
|
279
|
+
builder.save('wiggle_jello.gif', num_colors=128)
|
|
280
|
+
|
|
281
|
+
# Example 2: Wave
|
|
282
|
+
builder.clear()
|
|
283
|
+
frames = create_wiggle_animation(
|
|
284
|
+
object_type='emoji',
|
|
285
|
+
object_data={'emoji': '🌊', 'size': 100},
|
|
286
|
+
num_frames=30,
|
|
287
|
+
wiggle_type='wave',
|
|
288
|
+
intensity=1.2,
|
|
289
|
+
cycles=3
|
|
290
|
+
)
|
|
291
|
+
builder.add_frames(frames)
|
|
292
|
+
builder.save('wiggle_wave.gif', num_colors=128)
|
|
293
|
+
|
|
294
|
+
# Example 3: Excited wiggle (emoji size)
|
|
295
|
+
builder = GIFBuilder(width=128, height=128, fps=15)
|
|
296
|
+
frames = create_excited_wiggle(emoji='🎉', num_frames=20)
|
|
297
|
+
builder.add_frames(frames)
|
|
298
|
+
builder.save('wiggle_excited.gif', num_colors=48, optimize_for_emoji=True)
|
|
299
|
+
|
|
300
|
+
print("Created wiggle animations!")
|