DEV Community

platoro nical
platoro nical

Posted on

Considering lightweight formats that can be used for avatars and looping animations -01

Postscript: 1/29 0:20

Do you use lofi hip hop radio as background music?
Do you watch voiceroid videos? I like nikko-ken
Also, it seems that PNGTuber is popular

I'm always thinking about whether there is a format that is a little easier to handle, such as looping animations where only the avatar or part of the video moves.
As for the avatar, there was a similar solution, so I'll introduce the article.
https://note.com/lilish_works/n/n956ef62ea2e9

In this prototype, I used photoshop to export PNGs for each layer and write the coordinates into a json file, and then used pygame to output a player that plays those images.

This time, we used the Zundamon character from Mr. Sakamoto Duck.
https://www.pixiv.net/artworks/92641351
Thank you

The state on photoshop
Image from Gyazo

A group of files exported from Photoshop
Image from Gyazo

Demo played with pygames
Image from Gyazo
You can see that the layers are being recreated in the demo.

{“canvasWidth”:1082,“canvasHeight”:1650,“layers”: [ {“fileName”:“Path to file”, “x”: X coordinate from the top left of the canvas,“y”: Y coordinate from the top left of the canvas, “width“: width of image,”height“: height of image}, {”fileName“:”01.png“,”x“:258,”y“:109,”width“:549,”height":1473}, {“fileName”:“02.png”,“x”:460,“y”:474,“width”:66,“height”:61}, {“fileName”:“03.png”,“x”:466,“y”:496,“width”:58,“height”:16}, {“fileName”:“04.png”,“x”:480,“y”:487,“width”:33,“height”:39}, {“fileName”:“05.png”,“x”:366,“y”:351,“width”:250,“height”:107}, {“fileName”:“06.png”,“x”:383,“y”:305,“width”:216,“height”:18} ] } 
Enter fullscreen mode Exit fullscreen mode

The json is output as shown above

Next, I'm thinking of packing them as a single texture to make them easier to handle.
I've also extended the json format.

I'll use pillow to pack the textures.

pip install pillow 
Enter fullscreen mode Exit fullscreen mode

When packing, I'll also export the json.
At this time, I'll save the x and y coordinates from when the original sequential pngs were loaded separately.
Packed images
Image from Gyazo
The json that is output

{ “canvasWidth“: 1082, ”canvasHeight“: 1650, ”layers“: [ { ”fileName“: 01.png’, ”x“: 0, ”y“: 0, ”width“: 549, ”height“: 1473, ”basePosition_x“: 258, ”basePosition_y": 109 }, { “fileName”: “02.png”, “x”: 549, “y”: 0, “width”: 66, “height”: 61, “basePosition_x”: 460, “basePosition_y”: 474 }, { “fileName”: “03.png”, “x”: 615, “y”: 0, “width“: 58, ”height“: 16, ”basePosition_x“: 466, ”basePosition_y“: 496 }, { ”fileName“: ‘04.png’, ”x“: 673, ”y“: 0, ”width“: 33, ”height“: 39, ”basePosition_x": 480, “basePosition_y“: 487 }, { ”fileName“: 05.png’, ”x“: 706, ”y“: 0, ”width“: 250, ”height“: 107, ”basePosition_x“: 366, ”basePosition_y“: 351 }, { ”fileName": ‘06.png’, “x“: 0, ”y“: 1473, ”width“: 216, ”height“: 18, ”basePosition_x“: 383, ”basePosition_y": 305 } ] } 
Enter fullscreen mode Exit fullscreen mode

Next, I want to animate it

I'm having trouble

  • I can't get the layer name in photoshop jsx, so I get an error, so I'm making it a sequential number

If you know how to do this, please leave a comment!

If you know of any similar solutions, please let me know!

Extra
The following code was output by ChatGPT
It's just a demo, but if it's of any use

photoshop script

// Photoshop script: Output PNG for each layer and save position in JSON (map not used) #target photoshop try { // Let the user select the output folder var outputFolder = Folder.selectDialog(Please select the output folder.); if (!outputFolder) { alert(The output folder was not selected.); throw new Error(The output folder was not selected.); } // Array to store JSON data var layerData = []; // Get the current document var doc = app.activeDocument; // Get the document canvas size var canvasWidth = doc.width.as(px); var canvasHeight = doc.height.as(px); // Set the options for saving as PNG function saveAsPNG(file) { var pngOptions = new PNGSaveOptions(); pngOptions.interlaced = false; doc.saveAs(file, pngOptions, true, Extension.LOWERCASE); } // Alternative function for checking arrays function isArray(obj) { return Object.prototype.toString.call(obj) === [object Array]; } // Function to manually construct a JSON string function buildJSON(data) { var json = {; for (var key in data) { if (data.hasOwnProperty(key)) { var value = data[key]; if (typeof value === string) { value = '“‘ + value + ’”'; // Enclose strings in double quotes } else if (typeof value === object) { if (isArray(value)) { var arrayItems = []; for (var i = 0; i < value.length; i++) { arrayItems.push(buildJSON(value[i])); // Recursively process array elements } value = [ + arrayItems.join(,) + ]; } else { value = buildJSON(value); // Recursively process objects } } json += '“‘ + key + ’”:' + value + ,; } } json = json.replace(/,$/, “”); // Remove the last comma json += }; return json; } // Function to export layers individually (process in reverse order) function processLayers(layerSet, parentX, parentY, layerIndex) { for (var i = layerSet.layers.length - 1; i >= 0; i--) { // Process from bottom layer var layer = layerSet.layers[i]; // If the layer is a layer set, process recursively if (layer.typename === LayerSet) { layerIndex = processLayers(layer, parentX, parentY, layerIndex); } else { if (!layer.visible) continue; // Skip hidden layers // Get the layer's position var bounds = layer.bounds; var x = bounds[0].as(px) + parentX; var y = bounds[1].as(px) + parentY; // Duplicate the layer and create a new document app.activeDocument.activeLayer = layer; layer.copy(); var tempDoc = app.documents.add(layer.bounds[2] - layer.bounds[0], layer.bounds[3] - layer.bounds[1], doc.resolution, TempDoc, NewDocumentMode.RGB, DocumentFill.TRANSPARENT); tempDoc.paste(); // Create file names with sequential numbers var fileName = (00 + layerIndex).slice(-2) + .png; var filePath = new File(outputFolder + / + fileName); // Save as PNG saveAsPNG(filePath); // Add to JSON data layerData.push({ fileName: fileName, x: x, y: y, width: tempDoc.width.as(px), height: tempDoc.height.as(px) }); // Close temporary document tempDoc.close(SaveOptions.DONOTSAVECHANGES); // Increment the index for the next sequential number layerIndex++; } } return layerIndex; } // Process the layers (the sequential index starts from 1) processLayers(doc, 0, 0, 1); // Save as a JSON file if (layerData.length > 0) { var jsonFile = new File(outputFolder + /layer_data.json); jsonFile.open(w); // Manually construct JSON data var outputData = { canvasWidth: canvasWidth, canvasHeight: canvasHeight, layers: layerData }; jsonFile.write(buildJSON(outputData)); // Use the manual JSON construction function jsonFile.close(); alert(Layer export completed!); } else { alert(There were no processed layers.); } } catch (e) { alert("An error occurred: ” + e.message); } 
Enter fullscreen mode Exit fullscreen mode

pygame playback section
Additions
A texture packing button and a json switching button have been added.

pip install pygame 
Enter fullscreen mode Exit fullscreen mode
import pygame import json from PIL import Image # Default JSON file current_json_file = layer_data.json # Load JSON file with open(current_json_file, r) as file: data = json.load(file) # Get canvas size canvas_width = data[canvasWidth] canvas_height = data[canvasHeight] # Initialize Pygame pygame.init() screen = pygame.display.set_mode((canvas_width, canvas_height)) pygame.display.set_caption(Layer Drawing) # Dictionary to manage layer visibility layer_visibility = {layer[fileName]: True for layer in data[layers]} # Initialize font font = pygame.font.Font(None, 24) # Create texture function def create_texture(): texture = Image.new(RGBA, (canvas_width, canvas_height), (255, 255, 255, 0)) texture_layers = [] # Manage the current placement coordinates next_x = 0 next_y = 0 row_height = 0 for layer in data[layers]: if layer_visibility[layer[fileName]]: img = Image.open(layer[fileName]) img = img.resize((layer[width], layer[height])) # If a new line is needed if next_x + layer[width] > canvas_width: next_x = 0 next_y += row_height row_height = 0 # Add the image to be placed texture.paste(img, (next_x, next_y), img) texture_layers.append({ fileName: layer[fileName], x: next_x, y: next_y, width: layer[width], height: layer[height], basePosition_x: layer[x], basePosition_y: layer[y] }) # Calculate the next position next_x += layer[width] row_height = max(row_height, layer[height]) texture.save(texture.png) print(Texture image 'texture.png' has been created!") # Save the texture-compatible JSON texture_json = { “canvasWidth”: canvas_width, “canvasHeight”: canvas_height, “layers”: texture_layers } with open(“texture.json”, “w”) as json_file: json.dump(texture_json, json_file, indent=4) print(“Texture JSON ‘texture.json’ has been created!”) # Function to switch JSON files def switch_json(file_name): global data, layer_visibility, canvas_width, canvas_height, screen, current_json_file current_json_file = file_name with open(file_name, “r”) as file: data = json.load(file) canvas_width = data[“canvasWidth”] canvas_height = data[“canvasHeight”] screen = pygame.display.set_mode((canvas_width, canvas_height)) layer_visibility = {layer[“fileName”]: True for layer in data[“layers”]} print(f"{file_name} has been loaded! # Main loop running = True while running: for event in pygame.event.get(): if event.type == pygame.QUIT: running = False # Switch display on/off with mouse click if event.type == pygame.MOUSEBUTTONDOWN: x, y = event.pos button_height = 30 # Layer switching button for index, layer in enumerate(data[layers]): button_y = index * button_height if 10 <= x <= 200 and button_y <= y <= button_y + button_height: layer_visibility[layer[fileName]] = not layer_visibility[layer[fileName]] # Texture creation button texture_button_y = len(data[layers]) * button_height + 10 if 10 <= x <= 200 and texture_button_y <= y <= texture_button_y + button_height: create_texture() # JSON switching button switch_button_y = texture_button_y + 40 if 10 <= x <= 200 and switch_button_y <= y <= switch_button_y + button_height: if current_json_file == layer_data.json: switch_json(texture.json) else: switch_json(layer_data.json) screen.fill((255, 255, 255)) # Set the background to white  # Draw the layers for layer in data[layers]: if current_json_file == texture.json: if layer_visibility[layer[fileName]]: img = pygame.image.load(texture.png) cropped_img = img.subsurface(pygame.Rect(layer[x], layer[y], layer[width], layer[height])) screen.blit(cropped_img, (layer[basePosition_x], layer[basePosition_y])) elif layer_visibility[layer[fileName]]: img = pygame.image.load(layer[fileName]) # Load PNG file img = pygame.transform.scale(img, (layer[width], layer[height])) screen.blit(img, (layer[x], layer[y])) # Draw UI buttons for index, layer in enumerate(data[layers]): button_y = index * 30 color = (0, 200, 0) if layer_visibility[layer[fileName]] else (200, 0, 0) pygame.draw.rect(screen, color, (10, button_y, 190, 30)) text = font.render(layer[fileName], True, (255, 255, 255)) screen.blit(text, (15, button_y + 5)) # Texture creation button texture_button_y = len(data[layers]) * 30 + 10 pygame.draw.rect(screen, (0, 0, 200), (10, texture_button_y, 190, 30)) texture_text = font.render(Create Texture, True, (255, 255, 255)) screen.blit(texture_text, (15, texture_button_y + 5)) # JSON switching button switch_button_y = texture_button_y + 40 pygame.draw.rect(screen, (200, 200, 0), (10, switch_button_y, 190, 30)) if current_json_file == layer_data.json: switch_text = font.render(Switch to Texture JSON, True, (0, 0, 0)) else: switch_text = font.render(Switch to Layer Data JSON, True, (0, 0, 0)) screen.blit(switch_text, (15, switch_button_y + 5)) pygame.display.flip() pygame.quit() 
Enter fullscreen mode Exit fullscreen mode

pygame It looks pretty good
python drawing is really convenient

github
https://github.com/yuno-pxr/spriteAnimat

Top comments (0)