import numpy as np
import os
import time
from PIL import Image
from moviepy.editor import VideoClip

# Paths
image_one_path = '/home/daddy/morph/one.jpeg'
image_two_path = '/home/daddy/morph/two.png'
output_path = '/home/daddy/morph/output'
output_filename = f'morph{int(time.time())}.mp4'

# Function to create each frame of the video
def make_frame(t):
    if t < duration1:
        # Display the first image
        frame_array = img1_array.copy()
    elif t < duration1 + duration2:
        # Morphing effect with particle explosion
        progress = (t - duration1) / duration2  # Normalize progress to [0, 1]

        # Create an empty frame
        frame_array = np.zeros_like(img1_array)

        for idx, (x, y) in enumerate(particle_positions):
            # Extract patches from both images
            patch1 = img1_array[y:y+particle_size, x:x+particle_size]
            patch2 = img2_array[y:y+particle_size, x:x+particle_size]

            # Determine if in explosion or implosion phase
            if progress < 0.5:
                # Explosion phase
                explosion_progress = progress * 2  # Scale progress to [0, 1]
                current_x = x + explosion_progress * explosion_offsets[idx][0]
                current_y = y + explosion_progress * explosion_offsets[idx][1]
                current_patch = patch1
            else:
                # Implosion phase
                implosion_progress = (progress - 0.5) * 2  # Scale progress to [0, 1]
                exploded_x = x + explosion_offsets[idx][0]
                exploded_y = y + explosion_offsets[idx][1]
                current_x = exploded_x + implosion_progress * (x - exploded_x)
                current_y = exploded_y + implosion_progress * (y - exploded_y)
                current_patch = patch2

            # Convert positions to integers and ensure they stay within frame boundaries
            current_x = int(np.clip(current_x, 0, width - particle_size))
            current_y = int(np.clip(current_y, 0, height - particle_size))

            # Place the current patch onto the frame
            frame_array[current_y:current_y+particle_size, current_x:current_x+particle_size] = current_patch

    else:
        # Display the second image
        frame_array = img2_array.copy()

    # Convert the NumPy array back to an image
    frame_image = Image.fromarray(frame_array, 'RGBA')
    # Return frame as an RGB image (MoviePy expects RGB format)
    return np.array(frame_image.convert('RGB'))

# Main program
if __name__ == "__main__":
    # Input parameters with default values
    duration1 = int(input("Enter duration for first image display (seconds, default 5): ") or 5)
    duration2 = int(input("Enter duration for morphing effect (seconds, default 5): ") or 5)
    duration3 = int(input("Enter duration for second image display (seconds, default 5): ") or 5)
    particle_size = int(input("Enter particle size (pixels, default 4): ") or 4)

    # Load images and ensure they are in RGBA format
    img1 = Image.open(image_one_path).convert('RGBA').resize((1024, 1024))
    img2 = Image.open(image_two_path).convert('RGBA').resize((1024, 1024))

    # Convert images to NumPy arrays
    img1_array = np.array(img1)
    img2_array = np.array(img2)

    # Get image dimensions
    width, height = img1.size

    # Calculate the number of particles in the x and y directions
    particles_in_row = width // particle_size
    particles_in_col = height // particle_size

    # Prepare lists to store particle positions and explosion offsets
    particle_positions = []
    explosion_offsets = []

    # Calculate the center of the image
    center_x = width / 2
    center_y = height / 2

    # Generate particle positions and explosion offsets
    for i in range(particles_in_col):
        for j in range(particles_in_row):
            x = j * particle_size
            y = i * particle_size
            particle_positions.append((x, y))

            # Calculate vector from center to particle
            vector_x = x + particle_size / 2 - center_x
            vector_y = y + particle_size / 2 - center_y

            # Normalize the vector
            norm = np.hypot(vector_x, vector_y)
            if norm != 0:
                unit_vector_x = vector_x / norm
                unit_vector_y = vector_y / norm
            else:
                unit_vector_x = 0
                unit_vector_y = 0

            # Randomize explosion magnitude
            magnitude = np.random.uniform(50, 150)
            offset_x = unit_vector_x * magnitude
            offset_y = unit_vector_y * magnitude

            explosion_offsets.append((offset_x, offset_y))

    total_duration = duration1 + duration2 + duration3

    # Create and write the video
    animation = VideoClip(make_frame, duration=total_duration)
    animation.write_videofile(os.path.join(output_path, output_filename), fps=24)

Its working, but in a very basic way. The explosion needs to be bigger and the pieces need to morph/transform  into the image inside the blue outline. If that makes sense. Put on a show