639 lines
26 KiB
Python
639 lines
26 KiB
Python
import math
|
|
import numpy as np
|
|
from enum import Enum
|
|
from PIL import Image, ImageOps, ImageEnhance
|
|
from typing import Tuple, Callable, Dict, List, Any, Optional
|
|
import logging
|
|
|
|
logger = logging.getLogger(__name__)
|
|
|
|
class AnimationType(Enum):
|
|
"""Animation type options for GeekyRemB animations"""
|
|
NONE = "none"
|
|
BOUNCE = "bounce"
|
|
TRAVEL_LEFT = "travel_left"
|
|
TRAVEL_RIGHT = "travel_right"
|
|
ROTATE = "rotate"
|
|
FADE_IN = "fade_in"
|
|
FADE_OUT = "fade_out"
|
|
ZOOM_IN = "zoom_in"
|
|
ZOOM_OUT = "zoom_out"
|
|
SCALE_BOUNCE = "scale_bounce"
|
|
SPIRAL = "spiral"
|
|
SHAKE = "shake"
|
|
SLIDE_UP = "slide_up"
|
|
SLIDE_DOWN = "slide_down"
|
|
FLIP_HORIZONTAL = "flip_horizontal"
|
|
FLIP_VERTICAL = "flip_vertical"
|
|
WAVE = "wave"
|
|
PULSE = "pulse"
|
|
SWING = "swing"
|
|
SPIN = "spin"
|
|
FLASH = "flash"
|
|
KEYFRAME = "keyframe" # Animation type for keyframe-based animation
|
|
|
|
# Easing Functions
|
|
def linear(t):
|
|
"""Linear easing function"""
|
|
return t
|
|
|
|
def ease_in_quad(t):
|
|
"""Quadratic ease in"""
|
|
return t * t
|
|
|
|
def ease_out_quad(t):
|
|
"""Quadratic ease out"""
|
|
return t * (2 - t)
|
|
|
|
def ease_in_out_quad(t):
|
|
"""Quadratic ease in and out"""
|
|
return 2 * t * t if t < 0.5 else -1 + (4 - 2 * t) * t
|
|
|
|
def ease_in_cubic(t):
|
|
"""Cubic ease in"""
|
|
return t ** 3
|
|
|
|
def ease_out_cubic(t):
|
|
"""Cubic ease out"""
|
|
return (t - 1) ** 3 + 1
|
|
|
|
def ease_in_out_cubic(t):
|
|
"""Cubic ease in and out"""
|
|
return 4 * t * t * t if t < 0.5 else (t - 1) * (2 * t - 2) * (2 * t - 2) + 1
|
|
|
|
def ease_in_quart(t):
|
|
"""Quartic ease in"""
|
|
return t ** 4
|
|
|
|
def ease_out_quart(t):
|
|
"""Quartic ease out"""
|
|
return 1 - (t - 1) ** 4
|
|
|
|
def ease_in_out_quart(t):
|
|
"""Quartic ease in and out"""
|
|
return 8 * t ** 4 if t < 0.5 else 1 - 8 * (t - 1) ** 4
|
|
|
|
def ease_in_sine(t):
|
|
"""Sinusoidal ease in"""
|
|
return 1 - math.cos(t * math.pi / 2)
|
|
|
|
def ease_out_sine(t):
|
|
"""Sinusoidal ease out"""
|
|
return math.sin(t * math.pi / 2)
|
|
|
|
def ease_in_out_sine(t):
|
|
"""Sinusoidal ease in and out"""
|
|
return -(math.cos(math.pi * t) - 1) / 2
|
|
|
|
def ease_in_expo(t):
|
|
"""Exponential ease in"""
|
|
return 0 if t == 0 else 2 ** (10 * (t - 1))
|
|
|
|
def ease_out_expo(t):
|
|
"""Exponential ease out"""
|
|
return 1 if t == 1 else 1 - 2 ** (-10 * t)
|
|
|
|
def ease_in_out_expo(t):
|
|
"""Exponential ease in and out"""
|
|
if t == 0 or t == 1:
|
|
return t
|
|
t *= 2
|
|
if t < 1:
|
|
return 0.5 * 2 ** (10 * (t - 1))
|
|
t -= 1
|
|
return 0.5 * (2 - 2 ** (-10 * t))
|
|
|
|
def ease_in_elastic(t):
|
|
"""Elastic ease in"""
|
|
if t == 0 or t == 1:
|
|
return t
|
|
return -2 ** (10 * (t - 1)) * math.sin((t - 1.1) * 5 * math.pi)
|
|
|
|
def ease_out_elastic(t):
|
|
"""Elastic ease out"""
|
|
if t == 0 or t == 1:
|
|
return t
|
|
return 1 + 2 ** (-10 * t) * math.sin((t - 0.1) * 5 * math.pi)
|
|
|
|
def bounce(t):
|
|
"""Bounce easing function"""
|
|
if t < 4/11:
|
|
return (121 * t * t) / 16
|
|
elif t < 8/11:
|
|
return (363 / 40.0 * t * t) - (99 / 10.0 * t) + 17/5.0
|
|
elif t < 9/10:
|
|
return (4356 / 361.0 * t * t) - (35442 / 1805.0 * t) + 16061/1805.0
|
|
else:
|
|
return (54 / 5.0 * t * t) - (513 / 25.0 * t) + 268/25.0
|
|
|
|
def ease_out_bounce(t):
|
|
"""Bounce ease out"""
|
|
return bounce(t)
|
|
|
|
def ease_in_bounce(t):
|
|
"""Bounce ease in"""
|
|
return 1 - bounce(1 - t)
|
|
|
|
def ease_in_out_bounce(t):
|
|
"""Bounce ease in and out"""
|
|
if t < 0.5:
|
|
return 0.5 * (1 - bounce(1 - 2 * t))
|
|
else:
|
|
return 0.5 * bounce(2 * t - 1) + 0.5
|
|
|
|
# Dictionary of all available easing functions
|
|
EASING_FUNCTIONS = {
|
|
"linear": linear,
|
|
"ease_in_quad": ease_in_quad,
|
|
"ease_out_quad": ease_out_quad,
|
|
"ease_in_out_quad": ease_in_out_quad,
|
|
"ease_in_cubic": ease_in_cubic,
|
|
"ease_out_cubic": ease_out_cubic,
|
|
"ease_in_out_cubic": ease_in_out_cubic,
|
|
"ease_in_quart": ease_in_quart,
|
|
"ease_out_quart": ease_out_quart,
|
|
"ease_in_out_quart": ease_in_out_quart,
|
|
"ease_in_sine": ease_in_sine,
|
|
"ease_out_sine": ease_out_sine,
|
|
"ease_in_out_sine": ease_in_out_sine,
|
|
"ease_in_expo": ease_in_expo,
|
|
"ease_out_expo": ease_out_expo,
|
|
"ease_in_out_expo": ease_in_out_expo,
|
|
"ease_in_elastic": ease_in_elastic,
|
|
"ease_out_elastic": ease_out_elastic,
|
|
"ease_in_bounce": ease_in_bounce,
|
|
"ease_out_bounce": ease_out_bounce,
|
|
"ease_in_out_bounce": ease_in_out_bounce,
|
|
}
|
|
|
|
class EnhancedAnimator:
|
|
"""Enhanced animation processing with additional effects and keyframe support"""
|
|
|
|
@staticmethod
|
|
def interpolate_keyframes(keyframes, current_frame, fps):
|
|
"""
|
|
Interpolate position, scale, rotation, and opacity between keyframes
|
|
|
|
Args:
|
|
keyframes: List of keyframe dictionaries
|
|
current_frame: Current frame number
|
|
fps: Frames per second
|
|
|
|
Returns:
|
|
Dictionary with interpolated values for position, scale, rotation, opacity
|
|
"""
|
|
# Log keyframe input parameters for debugging
|
|
logger.info(f"Interpolating keyframes at frame {current_frame}")
|
|
logger.info(f"Number of keyframes: {len(keyframes)}")
|
|
|
|
if not keyframes or len(keyframes) == 0:
|
|
# Default values if no keyframes provided
|
|
logger.warning("No keyframes provided for interpolation")
|
|
return {
|
|
"position": (0, 0),
|
|
"scale": 1.0,
|
|
"rotation": 0.0,
|
|
"opacity": 1.0
|
|
}
|
|
|
|
# If only one keyframe, return its values
|
|
if len(keyframes) == 1:
|
|
kf = keyframes[0]
|
|
pos = kf.get("position", (0, 0))
|
|
logger.info(f"Only one keyframe available. Using position: {pos}")
|
|
return {
|
|
"position": pos,
|
|
"scale": kf.get("scale", 1.0),
|
|
"rotation": kf.get("rotation", 0.0),
|
|
"opacity": kf.get("opacity", 1.0)
|
|
}
|
|
|
|
# Sort keyframes by frame number
|
|
sorted_keyframes = sorted(keyframes, key=lambda k: k.get("frame", 0))
|
|
|
|
# Log sorted keyframes for debugging
|
|
for i, kf in enumerate(sorted_keyframes):
|
|
logger.info(f"Keyframe {i}: frame={kf.get('frame', 0)}, position={kf.get('position', (0, 0))}")
|
|
|
|
# Find keyframes that bound the current frame
|
|
for i in range(len(sorted_keyframes) - 1):
|
|
k1 = sorted_keyframes[i]
|
|
k2 = sorted_keyframes[i + 1]
|
|
|
|
frame1 = k1.get("frame", 0)
|
|
frame2 = k2.get("frame", 0)
|
|
|
|
if frame1 <= current_frame <= frame2:
|
|
# Calculate progress between keyframes
|
|
if frame1 == frame2: # Avoid division by zero
|
|
progress = 0
|
|
else:
|
|
progress = (current_frame - frame1) / (frame2 - frame1)
|
|
|
|
# Get easing function
|
|
easing_name = k2.get("easing", "linear")
|
|
easing_func = EASING_FUNCTIONS.get(easing_name, linear)
|
|
|
|
# Apply easing
|
|
eased_progress = easing_func(progress)
|
|
|
|
# Interpolate position
|
|
pos1 = k1.get("position", (0, 0))
|
|
pos2 = k2.get("position", (0, 0))
|
|
|
|
# Ensure positions are tuples with 2 elements
|
|
if not isinstance(pos1, tuple) or len(pos1) != 2:
|
|
logger.warning(f"Invalid position in keyframe {i}: {pos1}, using (0,0)")
|
|
pos1 = (0, 0)
|
|
if not isinstance(pos2, tuple) or len(pos2) != 2:
|
|
logger.warning(f"Invalid position in keyframe {i+1}: {pos2}, using (0,0)")
|
|
pos2 = (0, 0)
|
|
|
|
pos_x = pos1[0] + (pos2[0] - pos1[0]) * eased_progress
|
|
pos_y = pos1[1] + (pos2[1] - pos1[1]) * eased_progress
|
|
|
|
# Interpolate scale
|
|
scale1 = k1.get("scale", 1.0)
|
|
scale2 = k2.get("scale", 1.0)
|
|
scale = scale1 + (scale2 - scale1) * eased_progress
|
|
|
|
# Interpolate rotation
|
|
rot1 = k1.get("rotation", 0.0)
|
|
rot2 = k2.get("rotation", 0.0)
|
|
# Handle rotation wraparound for shortest path
|
|
if abs(rot2 - rot1) > 180:
|
|
if rot1 > rot2:
|
|
rot2 += 360
|
|
else:
|
|
rot1 += 360
|
|
rotation = rot1 + (rot2 - rot1) * eased_progress
|
|
rotation %= 360 # Keep in 0-360 range
|
|
|
|
# Interpolate opacity
|
|
op1 = k1.get("opacity", 1.0)
|
|
op2 = k2.get("opacity", 1.0)
|
|
opacity = op1 + (op2 - op1) * eased_progress
|
|
|
|
# Log interpolated values for debugging
|
|
logger.info(f"Interpolating between keyframes {i} and {i+1}")
|
|
logger.info(f"Frame1: {frame1}, Pos1: {pos1}, Frame2: {frame2}, Pos2: {pos2}")
|
|
logger.info(f"Current frame: {current_frame}, Progress: {progress}, Eased progress: {eased_progress}")
|
|
logger.info(f"Interpolated position: ({pos_x}, {pos_y})")
|
|
|
|
return {
|
|
"position": (pos_x, pos_y),
|
|
"scale": scale,
|
|
"rotation": rotation,
|
|
"opacity": opacity
|
|
}
|
|
|
|
# If current_frame is before first keyframe
|
|
if current_frame <= sorted_keyframes[0].get("frame", 0):
|
|
kf = sorted_keyframes[0]
|
|
pos = kf.get("position", (0, 0))
|
|
logger.info(f"Frame {current_frame} is before first keyframe. Using position: {pos}")
|
|
return {
|
|
"position": pos,
|
|
"scale": kf.get("scale", 1.0),
|
|
"rotation": kf.get("rotation", 0.0),
|
|
"opacity": kf.get("opacity", 1.0)
|
|
}
|
|
|
|
# If current_frame is after last keyframe
|
|
kf = sorted_keyframes[-1]
|
|
pos = kf.get("position", (0, 0))
|
|
logger.info(f"Frame {current_frame} is after last keyframe. Using position: {pos}")
|
|
return {
|
|
"position": pos,
|
|
"scale": kf.get("scale", 1.0),
|
|
"rotation": kf.get("rotation", 0.0),
|
|
"opacity": kf.get("opacity", 1.0)
|
|
}
|
|
|
|
@staticmethod
|
|
def animate_element(
|
|
element: Image.Image,
|
|
animation_type: str,
|
|
animation_speed: float,
|
|
frame_number: int,
|
|
total_frames: int,
|
|
x_start: int,
|
|
y_start: int,
|
|
canvas_width: int,
|
|
canvas_height: int,
|
|
scale: float,
|
|
rotation: float,
|
|
easing_func: Callable[[float], float],
|
|
repeat: int,
|
|
reverse: bool,
|
|
delay: float,
|
|
steps: int = 1,
|
|
phase_shift: float = 0.0,
|
|
keyframe_params: Dict = None
|
|
) -> Tuple[Image.Image, int, int]:
|
|
"""
|
|
Apply animation to an image frame.
|
|
"""
|
|
try:
|
|
# Check if we're doing keyframe animation - the string check is more robust than enum comparison
|
|
is_keyframe_animation = animation_type == "keyframe" and keyframe_params is not None
|
|
|
|
# If animation type is keyframe, use keyframe animation
|
|
if is_keyframe_animation:
|
|
# Log for debugging
|
|
logger.info(f"Using keyframe animation for frame {frame_number}")
|
|
|
|
# Validate keyframe parameters
|
|
if not keyframe_params or "keyframes" not in keyframe_params:
|
|
logger.warning("Invalid keyframe_params: missing keyframes list")
|
|
return element, x_start, y_start
|
|
|
|
# Get keyframes
|
|
keyframes = keyframe_params.get("keyframes", [])
|
|
|
|
if not keyframes:
|
|
logger.warning("Empty keyframes list")
|
|
return element, x_start, y_start
|
|
|
|
# Log keyframes for debugging
|
|
logger.info(f"Found {len(keyframes)} keyframes")
|
|
|
|
# Get FPS
|
|
fps = keyframe_params.get("fps", 30)
|
|
|
|
# Interpolate values based on current frame
|
|
interpolated = EnhancedAnimator.interpolate_keyframes(keyframes, frame_number, fps)
|
|
|
|
# Get interpolated values
|
|
x, y = interpolated["position"]
|
|
interp_scale = interpolated["scale"] * scale # Combine with base scale
|
|
interp_rotation = interpolated["rotation"] + rotation # Combine with base rotation
|
|
opacity = interpolated["opacity"]
|
|
|
|
# Log interpolated values for debugging
|
|
logger.info(f"Interpolated values for frame {frame_number}: position=({x}, {y}), scale={interp_scale}, rotation={interp_rotation}")
|
|
|
|
# Apply scale
|
|
orig_width, orig_height = element.size
|
|
new_size = (int(orig_width * interp_scale), int(orig_height * interp_scale))
|
|
element = element.resize(new_size, Image.LANCZOS)
|
|
|
|
# Apply rotation
|
|
if interp_rotation != 0:
|
|
element = element.rotate(
|
|
interp_rotation,
|
|
resample=Image.BICUBIC,
|
|
expand=True,
|
|
center=(element.width // 2, element.height // 2)
|
|
)
|
|
|
|
# Apply opacity if less than 1
|
|
if opacity < 1.0:
|
|
r, g, b, a = element.split()
|
|
a = a.point(lambda i: i * opacity)
|
|
element = Image.merge('RGBA', (r, g, b, a))
|
|
|
|
return element, int(x), int(y)
|
|
|
|
# Standard animation processing for non-keyframe animations
|
|
# Adjust frame_number based on delay
|
|
adjusted_frame = frame_number - int(delay * total_frames)
|
|
if adjusted_frame < 0:
|
|
return element, x_start, y_start # No animation yet
|
|
|
|
# Handle repeats
|
|
cycle_length = total_frames / max(1, repeat)
|
|
current_cycle = int(adjusted_frame / cycle_length)
|
|
frame_in_cycle = adjusted_frame % cycle_length
|
|
progress = frame_in_cycle / cycle_length # Normalized progress within the cycle
|
|
|
|
# Apply easing function
|
|
progress = easing_func(progress)
|
|
|
|
# Handle reverse
|
|
if reverse and current_cycle % 2 == 1:
|
|
progress = 1 - progress
|
|
|
|
orig_width, orig_height = element.size
|
|
|
|
if element.mode != 'RGBA':
|
|
element = element.convert('RGBA')
|
|
|
|
# Calculate the bounding box of visible pixels to determine rotation center
|
|
bbox = element.getbbox()
|
|
if bbox:
|
|
center_x = bbox[0] + (bbox[2] - bbox[0]) // 2
|
|
center_y = bbox[1] + (bbox[3] - bbox[1]) // 2
|
|
else:
|
|
center_x, center_y = element.width // 2, element.height // 2
|
|
|
|
# Apply scaling
|
|
new_size = (int(orig_width * scale), int(orig_height * scale))
|
|
element = element.resize(new_size, Image.LANCZOS)
|
|
|
|
# Apply rotation around the center of visible pixels
|
|
if rotation != 0:
|
|
element = element.rotate(
|
|
rotation,
|
|
resample=Image.BICUBIC,
|
|
expand=True,
|
|
center=(center_x, center_y)
|
|
)
|
|
|
|
x, y = x_start, y_start
|
|
|
|
# Apply steps and phase_shift for staggered animations
|
|
if steps > 1:
|
|
step_progress = progress * steps
|
|
current_step = int(step_progress)
|
|
progress = step_progress - current_step
|
|
x += int(phase_shift * current_step)
|
|
y += int(phase_shift * current_step)
|
|
progress = min(progress, 1.0)
|
|
|
|
# Process different animation types
|
|
if animation_type == AnimationType.BOUNCE.value:
|
|
y_offset = int(math.sin(progress * 2 * math.pi) * animation_speed * 50)
|
|
y += y_offset
|
|
|
|
elif animation_type == AnimationType.SCALE_BOUNCE.value:
|
|
scale_factor = 1 + math.sin(progress * 2 * math.pi) * animation_speed * 0.2
|
|
scaled_size = (int(element.width * scale_factor), int(element.height * scale_factor))
|
|
element = element.resize(scaled_size, Image.LANCZOS)
|
|
x -= (scaled_size[0] - new_size[0]) // 2
|
|
y -= (scaled_size[1] - new_size[1]) // 2
|
|
|
|
elif animation_type == AnimationType.SPIRAL.value:
|
|
radius = 50 * animation_speed
|
|
angle = progress * 4 * math.pi
|
|
x += int(radius * math.cos(angle))
|
|
y += int(radius * math.sin(angle))
|
|
element = element.rotate(
|
|
angle * 180 / math.pi,
|
|
resample=Image.BICUBIC,
|
|
expand=True,
|
|
center=(center_x, center_y)
|
|
)
|
|
|
|
elif animation_type == AnimationType.TRAVEL_LEFT.value:
|
|
x = int(canvas_width - (canvas_width + element.width) * progress)
|
|
|
|
elif animation_type == AnimationType.TRAVEL_RIGHT.value:
|
|
x = int(-element.width + (canvas_width + element.width) * progress)
|
|
|
|
elif animation_type == AnimationType.ROTATE.value:
|
|
spin_speed = 360 * animation_speed # degrees per cycle
|
|
angle = progress * spin_speed
|
|
element = element.rotate(
|
|
angle,
|
|
resample=Image.BICUBIC,
|
|
expand=True,
|
|
center=(center_x, center_y)
|
|
)
|
|
|
|
elif animation_type == AnimationType.FADE_IN.value:
|
|
opacity = int(progress * 255)
|
|
r, g, b, a = element.split()
|
|
a = a.point(lambda i: i * opacity // 255)
|
|
element = Image.merge('RGBA', (r, g, b, a))
|
|
|
|
elif animation_type == AnimationType.FADE_OUT.value:
|
|
opacity = int((1 - progress) * 255)
|
|
r, g, b, a = element.split()
|
|
a = a.point(lambda i: i * opacity // 255)
|
|
element = Image.merge('RGBA', (r, g, b, a))
|
|
|
|
elif animation_type == AnimationType.ZOOM_IN.value:
|
|
zoom_scale = 0.5 + progress * animation_speed
|
|
new_width = int(orig_width * scale * zoom_scale)
|
|
new_height = int(orig_height * scale * zoom_scale)
|
|
element = element.resize((new_width, new_height), Image.LANCZOS)
|
|
x = x_start - (new_width - orig_width * scale) // 2
|
|
y = y_start - (new_height - orig_height * scale) // 2
|
|
|
|
elif animation_type == AnimationType.ZOOM_OUT.value:
|
|
zoom_scale = 1.5 - progress * animation_speed
|
|
zoom_scale = max(0.1, zoom_scale) # Prevent negative or zero scaling
|
|
new_width = int(orig_width * scale * zoom_scale)
|
|
new_height = int(orig_height * scale * zoom_scale)
|
|
element = element.resize((new_width, new_height), Image.LANCZOS)
|
|
x = x_start - (new_width - orig_width * scale) // 2
|
|
y = y_start - (new_height - orig_height * scale) // 2
|
|
|
|
# New animation types
|
|
elif animation_type == AnimationType.SHAKE.value:
|
|
shake_amplitude = 10 * animation_speed
|
|
x_offset = int(math.sin(progress * 10 * math.pi) * shake_amplitude)
|
|
y_offset = int(math.cos(progress * 10 * math.pi) * shake_amplitude)
|
|
x += x_offset
|
|
y += y_offset
|
|
|
|
elif animation_type == AnimationType.SLIDE_UP.value:
|
|
start_y = canvas_height
|
|
end_y = y_start
|
|
y = int(start_y - (start_y - end_y) * progress)
|
|
|
|
elif animation_type == AnimationType.SLIDE_DOWN.value:
|
|
start_y = -element.height
|
|
end_y = y_start
|
|
y = int(start_y + (end_y - start_y) * progress)
|
|
|
|
elif animation_type == AnimationType.FLIP_HORIZONTAL.value:
|
|
# Create separate copies for before and after the flip
|
|
if progress < 0.5:
|
|
# Scale horizontally as we approach the middle
|
|
scale_x = 1 - progress * 2 # 1 -> 0
|
|
new_width = max(1, int(element.width * scale_x))
|
|
element = element.resize((new_width, element.height), Image.LANCZOS)
|
|
x = x_start + (orig_width * scale - new_width) // 2
|
|
else:
|
|
# Flip the image at the midpoint and scale back up
|
|
element = ImageOps.mirror(element)
|
|
scale_x = (progress - 0.5) * 2 # 0 -> 1
|
|
new_width = max(1, int(element.width * scale_x))
|
|
element = element.resize((new_width, element.height), Image.LANCZOS)
|
|
x = x_start + (orig_width * scale - new_width) // 2
|
|
|
|
elif animation_type == AnimationType.FLIP_VERTICAL.value:
|
|
# Similar approach for vertical flip
|
|
if progress < 0.5:
|
|
scale_y = 1 - progress * 2 # 1 -> 0
|
|
new_height = max(1, int(element.height * scale_y))
|
|
element = element.resize((element.width, new_height), Image.LANCZOS)
|
|
y = y_start + (orig_height * scale - new_height) // 2
|
|
else:
|
|
element = ImageOps.flip(element)
|
|
scale_y = (progress - 0.5) * 2 # 0 -> 1
|
|
new_height = max(1, int(element.height * scale_y))
|
|
element = element.resize((element.width, new_height), Image.LANCZOS)
|
|
y = y_start + (orig_height * scale - new_height) // 2
|
|
|
|
elif animation_type == AnimationType.WAVE.value:
|
|
# Create a wave effect by offsetting rows of pixels
|
|
img_array = np.array(element)
|
|
height, width = img_array.shape[:2]
|
|
|
|
# Create a new array for the waved image
|
|
waved = np.zeros_like(img_array)
|
|
|
|
# Wave amplitude and frequency
|
|
amplitude = 10 * animation_speed
|
|
frequency = 2 + 2 * animation_speed
|
|
|
|
# Apply horizontal wave
|
|
for i in range(height):
|
|
offset = int(amplitude * math.sin(2 * math.pi * frequency * i / height + progress * 2 * math.pi))
|
|
for j in range(width):
|
|
src_j = (j - offset) % width
|
|
waved[i, j] = img_array[i, src_j]
|
|
|
|
element = Image.fromarray(waved)
|
|
|
|
elif animation_type == AnimationType.PULSE.value:
|
|
pulse_scale = 1 + 0.3 * math.sin(progress * 4 * math.pi) * animation_speed
|
|
new_size = (int(element.width * pulse_scale), int(element.height * pulse_scale))
|
|
element = element.resize(new_size, Image.LANCZOS)
|
|
x -= (new_size[0] - element.width) // 2
|
|
y -= (new_size[1] - element.height) // 2
|
|
|
|
elif animation_type == AnimationType.SWING.value:
|
|
swing_amplitude = 15 * animation_speed
|
|
swing_angle = math.sin(progress * 4 * math.pi) * swing_amplitude
|
|
element = element.rotate(
|
|
swing_angle,
|
|
resample=Image.BICUBIC,
|
|
expand=True,
|
|
center=(center_x, center_y)
|
|
)
|
|
|
|
elif animation_type == AnimationType.SPIN.value:
|
|
spin_speed = 360 * animation_speed # degrees per cycle
|
|
angle = progress * spin_speed
|
|
element = element.rotate(
|
|
angle,
|
|
resample=Image.BICUBIC,
|
|
expand=True,
|
|
center=(center_x, center_y)
|
|
)
|
|
|
|
elif animation_type == AnimationType.FLASH.value:
|
|
# Flash between normal and bright
|
|
if progress < 0.5:
|
|
flash_strength = 1 + 3 * progress * animation_speed
|
|
else:
|
|
flash_strength = 1 + 3 * (1 - progress) * animation_speed
|
|
|
|
enhancer = ImageEnhance.Brightness(element)
|
|
element = enhancer.enhance(flash_strength)
|
|
|
|
# Log final position for debugging
|
|
if frame_number % 10 == 0: # Only log every 10th frame to reduce spam
|
|
logger.info(f"Standard animation frame {frame_number}: final position=({x}, {y})")
|
|
|
|
return element, x, y
|
|
|
|
except Exception as e:
|
|
logger.error(f"Animation error: {str(e)}")
|
|
import traceback
|
|
logger.error(traceback.format_exc())
|
|
return element, x_start, y_start |