My animation process toolset: the JSON configuration file

Animation
I have created a Python script to write a JSON file containing the basic config details of the animation.
Published

March 16, 2025

TL;DR

  • I have created a Python script to write a JSON file containing the basic config details of the animation.
  • I am using the timestamp of the created files as a proxy for the layer order.

In part 1 we created a tool to save mouse movement paths as JSON files. In part 2, I found a way to export a grouped and layered PSD from Photoshop into files and directories like this:

./Solar system 3000,3000
./Solar system 3000,3000/Planet and moon 954,527
./Solar system 3000,3000/Planet and moon 954,527/moon-593,413.png
./Solar system 3000,3000/Planet and moon 954,527/planet-593,413.png
./Solar system 3000,3000/Sun-954,527.png
./stars-0,0.png

3 directories, 4 files

Oh I’ve just spotted an issue. The order of the layers is important, but is not retained using this method. And there is no way to add the order using the Photoshop script as it. Let’s have a look at the code and see if we can fix this, to add a layer number at the start of the exported names. This will be an interesting test for Claude 3.7. The script is about 3.5k lines, and the change needed quite a simple on I think, although it may require multiple simple edits. Let’s see if it can do it… As I expected, that didn’t work. The script just wouldn’t execute, and I think you must need a developer version of Photoshop to enable debugging. Too much effort, so let’s just ignore that issue (our animation editor should make it easy to reorder layers if necessary). Also if we are lucky the creation timestamp of the files may be in the order we need. Let me check that:

find . -exec stat -f "%B %N" {} + | sort -n | cut -d' ' -f2-
.
./Solar system 3000,3000
./Solar system 3000,3000/Planet and moon 954,527
./Solar system 3000,3000/Planet and moon 954,527/moon-593,413.png
./Solar system 3000,3000/Planet and moon 954,527/planet-593,413.png
./Solar system 3000,3000/Sun-954,527.png
./stars-0,0.png

Yay! That’s in the correct order of the layers. Moving on…

Javascript running in the browser can’t read local files and directory structures. And we need a main JSON file with all the details exported from the PSD. So we need a python script to generate the JSON file. What might that file look like? I’ll make it by hand first, so I know what I need to ask Claude to write a Python script to do. Here’s what I think the file needs to look like:

[
  {
    "group": "Solar system",
    "centerX": 3000,
    "centerY": 3000,
    "children": [
      {
        "group": "Planet and moon",
        "centerX": 954,
        "centerY": 527,
        "children": [
          {
            "image": "moon.png",
            "centerX": 593,
            "centerY": 413
          },
          {
            "image": "planet.png",
            "centerX": 593,
            "centerY": 413
          }
        ]
      },
      {
        "image": "sun.png",
        "centerX": 954,
        "centerY": 527
      }
    ]
  },
  {
    "image": "stars.png",
    "centerX": 0,
    "centerY": 0
  }
]

So now I have my input, and expect output, so Claude should be able to write a Python script to convert the file structure to the JSON. I also want it to change the filenames too. This should be easy for it to do I think.

That worked! Here’s the script:


#!/usr/bin/env python3
import os
import json
import re
import sys
import shutil

def parse_name_coordinates(name):
    """Extract the name and coordinates from a directory or file name."""
    # Match pattern: name coordinates or name-coordinates
    match = re.search(r'(.+?)[ -](\d+),(\d+)(?:\.png)?$', name)
    if match:
        name, x, y = match.groups()
        # Remove any trailing hyphens from the name for files
        if name.endswith('-'):
            name = name[:-1]
        return name, int(x), int(y)
    return name, 0, 0

def process_directory(path, output_dir, file_mappings):
    """Recursively process directories and files to build the JSON structure."""
    items = []
    
    # Get all entries in the current directory
    entries = os.listdir(path)
    
    # Create a list of tuples (entry, timestamp, is_dir)
    entry_data = []
    for entry in entries:
        entry_path = os.path.join(path, entry)
        is_dir = os.path.isdir(entry_path)
        # Get creation time (or modification time as fallback)
        try:
            # For macOS
            timestamp = os.stat(entry_path).st_birthtime
        except AttributeError:
            # Fallback for other platforms that don't have creation time
            timestamp = os.path.getmtime(entry_path)
        
        entry_data.append((entry, timestamp, is_dir))
    
    # Sort by timestamp (creation time)
    entry_data.sort(key=lambda x: x[1])
    
    # Process directories and files in order of creation time
    for entry, _, is_dir in entry_data:
        entry_path = os.path.join(path, entry)
        
        if is_dir:
            # This is a directory
            dir_name, center_x, center_y = parse_name_coordinates(entry)
            
            # Create a group entry with children
            group_entry = {
                "group": dir_name,
                "centerX": center_x,
                "centerY": center_y,
                "children": process_directory(entry_path, output_dir, file_mappings)
            }
            items.append(group_entry)
        elif entry.lower().endswith('.png'):
            # This is a PNG file
            file_name, center_x, center_y = parse_name_coordinates(entry)
            
            # New filename for the output directory
            new_filename = f"{file_name}.png"
            
            # Save mapping of original file to new file
            file_mappings[os.path.join(path, entry)] = os.path.join(output_dir, new_filename)
            
            # Create an image entry
            image_entry = {
                "image": new_filename,
                "centerX": center_x,
                "centerY": center_y
            }
            items.append(image_entry)
    
    return items

def main():
    if len(sys.argv) < 2:
        print("Usage: python script.py <input_directory> [output_directory]")
        sys.exit(1)
    
    input_dir = sys.argv[1]
    output_dir = sys.argv[2] if len(sys.argv) > 2 else "output"
    
    # Ensure output_dir is treated as a directory name, not a filename
    if not os.path.isdir(output_dir):
        try:
            os.makedirs(output_dir, exist_ok=True)
        except Exception as e:
            print(f"Error creating output directory: {e}")
            sys.exit(1)
    
    # Define output JSON file path inside the directory
    output_json_file = os.path.join(output_dir, "animation_data.json")
    
    # Dictionary to track file mappings (original path -> new path)
    file_mappings = {}
    
    # Create an empty result list
    result = []
    
    # Get all entries in the input directory with timestamps
    entries = os.listdir(input_dir)
    entry_data = []
    
    for entry in entries:
        entry_path = os.path.join(input_dir, entry)
        is_dir = os.path.isdir(entry_path)
        
        # Get creation time (or modification time as fallback)
        try:
            # For macOS
            timestamp = os.stat(entry_path).st_birthtime
        except AttributeError:
            # Fallback for other platforms that don't have creation time
            timestamp = os.path.getmtime(entry_path)
            
        entry_data.append((entry, timestamp, is_dir))
    
    # Sort by timestamp (creation time)
    entry_data.sort(key=lambda x: x[1])
    
    # Process entries in order of creation time
    for entry, _, is_dir in entry_data:
        entry_path = os.path.join(input_dir, entry)
        
        if is_dir:
            # This is a directory like "Solar system 3000,3000"
            dir_name, center_x, center_y = parse_name_coordinates(entry)
            
            # Create a group entry with children
            group_entry = {
                "group": dir_name,
                "centerX": center_x,
                "centerY": center_y,
                "children": process_directory(entry_path, output_dir, file_mappings)
            }
            result.append(group_entry)
        elif os.path.isfile(entry_path) and entry.lower().endswith('.png'):
            # This is a file like "stars-0,0.png" at the root level
            file_name, center_x, center_y = parse_name_coordinates(entry)
            
            # New filename for the output directory
            new_filename = f"{file_name}.png"
            
            # Save mapping of original file to new file
            file_mappings[entry_path] = os.path.join(output_dir, new_filename)
            
            # Create an image entry
            image_entry = {
                "image": new_filename,
                "centerX": center_x,
                "centerY": center_y
            }
            result.append(image_entry)
    
    # Write the JSON output
    with open(output_json_file, 'w') as f:
        json.dump(result, f, indent=2)
    
    # Copy all PNG files to the output directory with their new names
    for src_path, dest_path in file_mappings.items():
        shutil.copy2(src_path, dest_path)
        print(f"Copied: {os.path.basename(src_path)}{os.path.basename(dest_path)}")
    
    print(f"JSON data has been written to {output_json_file}")
    print(f"All files have been copied to the {output_dir} directory")

if __name__ == "__main__":
    main()

To run just give it the directory of the export from Photoshop:

python create-json01.py "./export"

I think I’m finally at the point when I can start creating the tool that actually does the animating.