commit c8feceedcabb1fe0cbe2ad22e89f6e3159b032c7 Author: warlock Date: Sat Oct 11 22:23:43 2025 +0530 init diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..9978a9e --- /dev/null +++ b/.gitignore @@ -0,0 +1,66 @@ +# Node modules +node_modules/ + +# Dependency directories +jspm_packages/ + +# Optional npm cache directory +.npm + +# Optional eslint cache +.eslintcache + +# Optional REPL history +.node_repl_history + +# Output of 'npm pack' +*.tgz + +# Logs +logs +*.log +npm-debug.log* +yarn-debug.log* +yarn-error.log* + +# Directory for instrumented libs generated by jscoverage/JSCover +lib-cov/ + +# Coverage directory used by testing tools +coverage/ +.nyc_output/ + +# Debug folders +debug/ +dist/ +build/ +tmp/ +temp/ +.out/ +out/ + +# Project local env files +.env +.env.* + +# VS Code settings +.vscode/ + +# OSX system files +.DS_Store + +# IDE files +.idea/ +*.sublime* +*.sw? +*.iml + +# Generated replay folders or images (based on your code context) +*.png +replays/ +maps/ +top/ +side/ +front/ +/*.png + diff --git a/2025-10-06_18-00-42/create_4panel_video.py b/2025-10-06_18-00-42/create_4panel_video.py new file mode 100755 index 0000000..205acf4 --- /dev/null +++ b/2025-10-06_18-00-42/create_4panel_video.py @@ -0,0 +1,125 @@ +#!/usr/bin/env python3 +""" +Script to create a 4-panel video from front, side, and top view images. +Layout: +- Top Left: Front view +- Top Right: Side view +- Bottom Left: Top view +- Bottom Right: Empty (black) for future content +""" + +import cv2 +import numpy as np +import os +import glob +from pathlib import Path + +def create_4panel_video(): + # Define paths + base_path = Path("/home/warlock/Projects/ScfHeatmapGen/js/2025-10-06_18-00-42") + front_dir = base_path / "front" + side_dir = base_path / "side" + top_dir = base_path / "top" + output_path = base_path / "combined_4panel_video.mp4" + + # Get all image files from each directory + front_images = sorted(glob.glob(str(front_dir / "*.png"))) + side_images = sorted(glob.glob(str(side_dir / "*.png"))) + top_images = sorted(glob.glob(str(top_dir / "*.png"))) + + print(f"Found {len(front_images)} front images") + print(f"Found {len(side_images)} side images") + print(f"Found {len(top_images)} top images") + + # Check if all directories have the same number of images + if not (len(front_images) == len(side_images) == len(top_images)): + print("Warning: Different number of images in directories!") + min_count = min(len(front_images), len(side_images), len(top_images)) + front_images = front_images[:min_count] + side_images = side_images[:min_count] + top_images = top_images[:min_count] + print(f"Using {min_count} images from each directory") + + # Read first image to get dimensions + first_img = cv2.imread(front_images[0]) + if first_img is None: + print(f"Error: Could not read image {front_images[0]}") + return + + img_height, img_width = first_img.shape[:2] + print(f"Image dimensions: {img_width}x{img_height}") + + # Calculate panel dimensions (2x2 grid) + panel_width = img_width + panel_height = img_height + + # Create video writer + fourcc = cv2.VideoWriter_fourcc(*'mp4v') + fps = 30 # Adjust frame rate as needed + + # Create output video with 2x2 panel layout + output_width = panel_width * 2 + output_height = panel_height * 2 + + out = cv2.VideoWriter(str(output_path), fourcc, fps, (output_width, output_height)) + + print(f"Creating video: {output_width}x{output_height} at {fps} FPS") + print(f"Output file: {output_path}") + + # Process each frame + for i, (front_img_path, side_img_path, top_img_path) in enumerate(zip(front_images, side_images, top_images)): + # Read images + front_img = cv2.imread(front_img_path) + side_img = cv2.imread(side_img_path) + top_img = cv2.imread(top_img_path) + + if front_img is None or side_img is None or top_img is None: + print(f"Warning: Could not read images for frame {i+1}") + continue + + # Resize images to panel size if needed + front_img = cv2.resize(front_img, (panel_width, panel_height)) + side_img = cv2.resize(side_img, (panel_width, panel_height)) + top_img = cv2.resize(top_img, (panel_width, panel_height)) + + # Create empty panel for bottom right + empty_panel = np.zeros((panel_height, panel_width, 3), dtype=np.uint8) + + # Create 2x2 grid + # Top row + top_row = np.hstack([front_img, side_img]) + # Bottom row + bottom_row = np.hstack([top_img, empty_panel]) + # Combine rows + combined_frame = np.vstack([top_row, bottom_row]) + + # Add labels to each panel + font = cv2.FONT_HERSHEY_SIMPLEX + font_scale = 1.0 + color = (255, 255, 255) # White + thickness = 2 + + # Add labels + cv2.putText(combined_frame, "Front View", (10, 30), font, font_scale, color, thickness) + cv2.putText(combined_frame, "Side View", (panel_width + 10, 30), font, font_scale, color, thickness) + cv2.putText(combined_frame, "Top View", (10, panel_height + 30), font, font_scale, color, thickness) + cv2.putText(combined_frame, "Reserved", (panel_width + 10, panel_height + 30), font, font_scale, color, thickness) + + # Write frame + out.write(combined_frame) + + # Progress indicator + if (i + 1) % 10 == 0: + print(f"Processed {i + 1}/{len(front_images)} frames") + + # Release everything + out.release() + cv2.destroyAllWindows() + + print(f"\nVideo creation completed!") + print(f"Output saved to: {output_path}") + print(f"Total frames: {len(front_images)}") + print(f"Duration: {len(front_images)/fps:.2f} seconds") + +if __name__ == "__main__": + create_4panel_video() diff --git a/2025-10-06_18-00-42/create_4panel_video.sh b/2025-10-06_18-00-42/create_4panel_video.sh new file mode 100755 index 0000000..c8ae846 --- /dev/null +++ b/2025-10-06_18-00-42/create_4panel_video.sh @@ -0,0 +1,96 @@ +#!/bin/bash + +# Script to create a 4-panel video from front, side, and top view images +# Layout: +# - Top Left: Front view +# - Top Right: Side view +# - Bottom Left: Top view +# - Bottom Right: Empty (black) for future content + +set -e + +BASE_PATH="/home/warlock/Projects/ScfHeatmapGen/js/2025-10-06_18-00-42" +FRONT_DIR="$BASE_PATH/front" +SIDE_DIR="$BASE_PATH/side" +TOP_DIR="$BASE_PATH/top" +OUTPUT_DIR="$BASE_PATH/temp_frames" +OUTPUT_VIDEO="$BASE_PATH/combined_4panel_video.mp4" + +echo "Creating 4-panel video from heatmap images..." + +# Create temporary directory for combined frames +mkdir -p "$OUTPUT_DIR" + +# Get the number of images (assuming all directories have the same count) +NUM_IMAGES=$(ls "$FRONT_DIR"/*.png | wc -l) +echo "Found $NUM_IMAGES images in each directory" + +# Get dimensions of first image +FIRST_IMG=$(ls "$FRONT_DIR"/*.png | head -1) +IMG_INFO=$(identify "$FIRST_IMG") +IMG_WIDTH=$(echo "$IMG_INFO" | cut -d' ' -f3 | cut -d'x' -f1) +IMG_HEIGHT=$(echo "$IMG_INFO" | cut -d' ' -f3 | cut -d'x' -f2) + +echo "Image dimensions: ${IMG_WIDTH}x${IMG_HEIGHT}" + +# Calculate output dimensions (2x2 grid) +OUTPUT_WIDTH=$((IMG_WIDTH * 2)) +OUTPUT_HEIGHT=$((IMG_HEIGHT * 2)) + +echo "Output video dimensions: ${OUTPUT_WIDTH}x${OUTPUT_HEIGHT}" + +# Process each frame +for i in $(seq -w 1 $NUM_IMAGES); do + FRONT_IMG="$FRONT_DIR/${i}.png" + SIDE_IMG="$SIDE_DIR/${i}.png" + TOP_IMG="$TOP_DIR/${i}.png" + OUTPUT_FRAME="$OUTPUT_DIR/frame_${i}.png" + + echo "Processing frame $i/$NUM_IMAGES" + + # Create a black panel for the bottom right + convert -size ${IMG_WIDTH}x${IMG_HEIGHT} xc:black "$OUTPUT_DIR/empty_${i}.png" + + # Create 2x2 grid using ImageMagick montage + montage \ + "$FRONT_IMG" "$SIDE_IMG" \ + "$TOP_IMG" "$OUTPUT_DIR/empty_${i}.png" \ + -tile 2x2 \ + -geometry ${IMG_WIDTH}x${IMG_HEIGHT}+0+0 \ + -background black \ + "$OUTPUT_FRAME" + + # Add labels to each panel + convert "$OUTPUT_FRAME" \ + -font DejaVu-Sans-Bold \ + -pointsize 24 \ + -fill white \ + -annotate +20+30 "Front View" \ + -annotate +$((IMG_WIDTH + 20))+30 "Side View" \ + -annotate +20+$((IMG_HEIGHT + 30)) "Top View" \ + -annotate +$((IMG_WIDTH + 20))+$((IMG_HEIGHT + 30)) "Reserved" \ + "$OUTPUT_FRAME" + + # Clean up temporary empty panel + rm "$OUTPUT_DIR/empty_${i}.png" +done + +echo "All frames processed. Creating video..." + +# Create video from frames using FFmpeg +ffmpeg -y \ + -framerate 2 \ + -i "$OUTPUT_DIR/frame_%03d.png" \ + -c:v libopenh264 \ + -pix_fmt yuv420p \ + -crf 18 \ + "$OUTPUT_VIDEO" + +echo "Video created: $OUTPUT_VIDEO" + +# # Clean up temporary frames +# echo "Cleaning up temporary files..." +# rm -rf "$OUTPUT_DIR" + +echo "Done! Video saved to: $OUTPUT_VIDEO" +echo "Duration: $(echo "scale=2; $NUM_IMAGES/30" | bc) seconds" diff --git a/2025-10-06_18-00-42/temp_frames/output.mp4 b/2025-10-06_18-00-42/temp_frames/output.mp4 new file mode 100644 index 0000000..ccb6b20 Binary files /dev/null and b/2025-10-06_18-00-42/temp_frames/output.mp4 differ diff --git a/package-lock.json b/package-lock.json new file mode 100644 index 0000000..9d1a91f --- /dev/null +++ b/package-lock.json @@ -0,0 +1,492 @@ +{ + "name": "canvas-hello-world", + "version": "1.0.0", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "canvas-hello-world", + "version": "1.0.0", + "dependencies": { + "canvas": "^3.2.0" + }, + "devDependencies": { + "@types/node": "^20.0.0", + "typescript": "^5.0.0" + } + }, + "node_modules/@types/node": { + "version": "20.19.19", + "resolved": "https://registry.npmjs.org/@types/node/-/node-20.19.19.tgz", + "integrity": "sha512-pb1Uqj5WJP7wrcbLU7Ru4QtA0+3kAXrkutGiD26wUKzSMgNNaPARTUDQmElUXp64kh3cWdou3Q0C7qwwxqSFmg==", + "dev": true, + "license": "MIT", + "dependencies": { + "undici-types": "~6.21.0" + } + }, + "node_modules/base64-js": { + "version": "1.5.1", + "resolved": "https://registry.npmjs.org/base64-js/-/base64-js-1.5.1.tgz", + "integrity": "sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT" + }, + "node_modules/bl": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/bl/-/bl-4.1.0.tgz", + "integrity": "sha512-1W07cM9gS6DcLperZfFSj+bWLtaPGSOHWhPiGzXmvVJbRLdG82sH/Kn8EtW1VqWVA54AKf2h5k5BbnIbwF3h6w==", + "license": "MIT", + "dependencies": { + "buffer": "^5.5.0", + "inherits": "^2.0.4", + "readable-stream": "^3.4.0" + } + }, + "node_modules/buffer": { + "version": "5.7.1", + "resolved": "https://registry.npmjs.org/buffer/-/buffer-5.7.1.tgz", + "integrity": "sha512-EHcyIPBQ4BSGlvjB16k5KgAJ27CIsHY/2JBmCRReo48y9rQ3MaUzWX3KVlBa4U7MyX02HdVj0K7C3WaB3ju7FQ==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT", + "dependencies": { + "base64-js": "^1.3.1", + "ieee754": "^1.1.13" + } + }, + "node_modules/canvas": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/canvas/-/canvas-3.2.0.tgz", + "integrity": "sha512-jk0GxrLtUEmW/TmFsk2WghvgHe8B0pxGilqCL21y8lHkPUGa6FTsnCNtHPOzT8O3y+N+m3espawV80bbBlgfTA==", + "hasInstallScript": true, + "license": "MIT", + "dependencies": { + "node-addon-api": "^7.0.0", + "prebuild-install": "^7.1.3" + }, + "engines": { + "node": "^18.12.0 || >= 20.9.0" + } + }, + "node_modules/chownr": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/chownr/-/chownr-1.1.4.tgz", + "integrity": "sha512-jJ0bqzaylmJtVnNgzTeSOs8DPavpbYgEr/b0YL8/2GO3xJEhInFmhKMUnEJQjZumK7KXGFhUy89PrsJWlakBVg==", + "license": "ISC" + }, + "node_modules/decompress-response": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/decompress-response/-/decompress-response-6.0.0.tgz", + "integrity": "sha512-aW35yZM6Bb/4oJlZncMH2LCoZtJXTRxES17vE3hoRiowU2kWHaJKFkSBDnDR+cm9J+9QhXmREyIfv0pji9ejCQ==", + "license": "MIT", + "dependencies": { + "mimic-response": "^3.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/deep-extend": { + "version": "0.6.0", + "resolved": "https://registry.npmjs.org/deep-extend/-/deep-extend-0.6.0.tgz", + "integrity": "sha512-LOHxIOaPYdHlJRtCQfDIVZtfw/ufM8+rVj649RIHzcm/vGwQRXFt6OPqIFWsm2XEMrNIEtWR64sY1LEKD2vAOA==", + "license": "MIT", + "engines": { + "node": ">=4.0.0" + } + }, + "node_modules/detect-libc": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-2.1.2.tgz", + "integrity": "sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ==", + "license": "Apache-2.0", + "engines": { + "node": ">=8" + } + }, + "node_modules/end-of-stream": { + "version": "1.4.5", + "resolved": "https://registry.npmjs.org/end-of-stream/-/end-of-stream-1.4.5.tgz", + "integrity": "sha512-ooEGc6HP26xXq/N+GCGOT0JKCLDGrq2bQUZrQ7gyrJiZANJ/8YDTxTpQBXGMn+WbIQXNVpyWymm7KYVICQnyOg==", + "license": "MIT", + "dependencies": { + "once": "^1.4.0" + } + }, + "node_modules/expand-template": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/expand-template/-/expand-template-2.0.3.tgz", + "integrity": "sha512-XYfuKMvj4O35f/pOXLObndIRvyQ+/+6AhODh+OKWj9S9498pHHn/IMszH+gt0fBCRWMNfk1ZSp5x3AifmnI2vg==", + "license": "(MIT OR WTFPL)", + "engines": { + "node": ">=6" + } + }, + "node_modules/fs-constants": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/fs-constants/-/fs-constants-1.0.0.tgz", + "integrity": "sha512-y6OAwoSIf7FyjMIv94u+b5rdheZEjzR63GTyZJm5qh4Bi+2YgwLCcI/fPFZkL5PSixOt6ZNKm+w+Hfp/Bciwow==", + "license": "MIT" + }, + "node_modules/github-from-package": { + "version": "0.0.0", + "resolved": "https://registry.npmjs.org/github-from-package/-/github-from-package-0.0.0.tgz", + "integrity": "sha512-SyHy3T1v2NUXn29OsWdxmK6RwHD+vkj3v8en8AOBZ1wBQ/hCAQ5bAQTD02kW4W9tUp/3Qh6J8r9EvntiyCmOOw==", + "license": "MIT" + }, + "node_modules/ieee754": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/ieee754/-/ieee754-1.2.1.tgz", + "integrity": "sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "BSD-3-Clause" + }, + "node_modules/inherits": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz", + "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==", + "license": "ISC" + }, + "node_modules/ini": { + "version": "1.3.8", + "resolved": "https://registry.npmjs.org/ini/-/ini-1.3.8.tgz", + "integrity": "sha512-JV/yugV2uzW5iMRSiZAyDtQd+nxtUnjeLt0acNdw98kKLrvuRVyB80tsREOE7yvGVgalhZ6RNXCmEHkUKBKxew==", + "license": "ISC" + }, + "node_modules/mimic-response": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/mimic-response/-/mimic-response-3.1.0.tgz", + "integrity": "sha512-z0yWI+4FDrrweS8Zmt4Ej5HdJmky15+L2e6Wgn3+iK5fWzb6T3fhNFq2+MeTRb064c6Wr4N/wv0DzQTjNzHNGQ==", + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/minimist": { + "version": "1.2.8", + "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.8.tgz", + "integrity": "sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA==", + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/mkdirp-classic": { + "version": "0.5.3", + "resolved": "https://registry.npmjs.org/mkdirp-classic/-/mkdirp-classic-0.5.3.tgz", + "integrity": "sha512-gKLcREMhtuZRwRAfqP3RFW+TK4JqApVBtOIftVgjuABpAtpxhPGaDcfvbhNvD0B8iD1oUr/txX35NjcaY6Ns/A==", + "license": "MIT" + }, + "node_modules/napi-build-utils": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/napi-build-utils/-/napi-build-utils-2.0.0.tgz", + "integrity": "sha512-GEbrYkbfF7MoNaoh2iGG84Mnf/WZfB0GdGEsM8wz7Expx/LlWf5U8t9nvJKXSp3qr5IsEbK04cBGhol/KwOsWA==", + "license": "MIT" + }, + "node_modules/node-abi": { + "version": "3.78.0", + "resolved": "https://registry.npmjs.org/node-abi/-/node-abi-3.78.0.tgz", + "integrity": "sha512-E2wEyrgX/CqvicaQYU3Ze1PFGjc4QYPGsjUrlYkqAE0WjHEZwgOsGMPMzkMse4LjJbDmaEuDX3CM036j5K2DSQ==", + "license": "MIT", + "dependencies": { + "semver": "^7.3.5" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/node-addon-api": { + "version": "7.1.1", + "resolved": "https://registry.npmjs.org/node-addon-api/-/node-addon-api-7.1.1.tgz", + "integrity": "sha512-5m3bsyrjFWE1xf7nz7YXdN4udnVtXK6/Yfgn5qnahL6bCkf2yKt4k3nuTKAtT4r3IG8JNR2ncsIMdZuAzJjHQQ==", + "license": "MIT" + }, + "node_modules/once": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz", + "integrity": "sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==", + "license": "ISC", + "dependencies": { + "wrappy": "1" + } + }, + "node_modules/prebuild-install": { + "version": "7.1.3", + "resolved": "https://registry.npmjs.org/prebuild-install/-/prebuild-install-7.1.3.tgz", + "integrity": "sha512-8Mf2cbV7x1cXPUILADGI3wuhfqWvtiLA1iclTDbFRZkgRQS0NqsPZphna9V+HyTEadheuPmjaJMsbzKQFOzLug==", + "license": "MIT", + "dependencies": { + "detect-libc": "^2.0.0", + "expand-template": "^2.0.3", + "github-from-package": "0.0.0", + "minimist": "^1.2.3", + "mkdirp-classic": "^0.5.3", + "napi-build-utils": "^2.0.0", + "node-abi": "^3.3.0", + "pump": "^3.0.0", + "rc": "^1.2.7", + "simple-get": "^4.0.0", + "tar-fs": "^2.0.0", + "tunnel-agent": "^0.6.0" + }, + "bin": { + "prebuild-install": "bin.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/pump": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/pump/-/pump-3.0.3.tgz", + "integrity": "sha512-todwxLMY7/heScKmntwQG8CXVkWUOdYxIvY2s0VWAAMh/nd8SoYiRaKjlr7+iCs984f2P8zvrfWcDDYVb73NfA==", + "license": "MIT", + "dependencies": { + "end-of-stream": "^1.1.0", + "once": "^1.3.1" + } + }, + "node_modules/rc": { + "version": "1.2.8", + "resolved": "https://registry.npmjs.org/rc/-/rc-1.2.8.tgz", + "integrity": "sha512-y3bGgqKj3QBdxLbLkomlohkvsA8gdAiUQlSBJnBhfn+BPxg4bc62d8TcBW15wavDfgexCgccckhcZvywyQYPOw==", + "license": "(BSD-2-Clause OR MIT OR Apache-2.0)", + "dependencies": { + "deep-extend": "^0.6.0", + "ini": "~1.3.0", + "minimist": "^1.2.0", + "strip-json-comments": "~2.0.1" + }, + "bin": { + "rc": "cli.js" + } + }, + "node_modules/readable-stream": { + "version": "3.6.2", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-3.6.2.tgz", + "integrity": "sha512-9u/sniCrY3D5WdsERHzHE4G2YCXqoG5FTHUiCC4SIbr6XcLZBY05ya9EKjYek9O5xOAwjGq+1JdGBAS7Q9ScoA==", + "license": "MIT", + "dependencies": { + "inherits": "^2.0.3", + "string_decoder": "^1.1.1", + "util-deprecate": "^1.0.1" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/safe-buffer": { + "version": "5.2.1", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz", + "integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT" + }, + "node_modules/semver": { + "version": "7.7.2", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.2.tgz", + "integrity": "sha512-RF0Fw+rO5AMf9MAyaRXI4AV0Ulj5lMHqVxxdSgiVbixSCXoEmmX/jk0CuJw4+3SqroYO9VoUh+HcuJivvtJemA==", + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/simple-concat": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/simple-concat/-/simple-concat-1.0.1.tgz", + "integrity": "sha512-cSFtAPtRhljv69IK0hTVZQ+OfE9nePi/rtJmw5UjHeVyVroEqJXP1sFztKUy1qU+xvz3u/sfYJLa947b7nAN2Q==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT" + }, + "node_modules/simple-get": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/simple-get/-/simple-get-4.0.1.tgz", + "integrity": "sha512-brv7p5WgH0jmQJr1ZDDfKDOSeWWg+OVypG99A/5vYGPqJ6pxiaHLy8nxtFjBA7oMa01ebA9gfh1uMCFqOuXxvA==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT", + "dependencies": { + "decompress-response": "^6.0.0", + "once": "^1.3.1", + "simple-concat": "^1.0.0" + } + }, + "node_modules/string_decoder": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.3.0.tgz", + "integrity": "sha512-hkRX8U1WjJFd8LsDJ2yQ/wWWxaopEsABU1XfkM8A+j0+85JAGppt16cr1Whg6KIbb4okU6Mql6BOj+uup/wKeA==", + "license": "MIT", + "dependencies": { + "safe-buffer": "~5.2.0" + } + }, + "node_modules/strip-json-comments": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-2.0.1.tgz", + "integrity": "sha512-4gB8na07fecVVkOI6Rs4e7T6NOTki5EmL7TUduTs6bu3EdnSycntVJ4re8kgZA+wx9IueI2Y11bfbgwtzuE0KQ==", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/tar-fs": { + "version": "2.1.4", + "resolved": "https://registry.npmjs.org/tar-fs/-/tar-fs-2.1.4.tgz", + "integrity": "sha512-mDAjwmZdh7LTT6pNleZ05Yt65HC3E+NiQzl672vQG38jIrehtJk/J3mNwIg+vShQPcLF/LV7CMnDW6vjj6sfYQ==", + "license": "MIT", + "dependencies": { + "chownr": "^1.1.1", + "mkdirp-classic": "^0.5.2", + "pump": "^3.0.0", + "tar-stream": "^2.1.4" + } + }, + "node_modules/tar-stream": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/tar-stream/-/tar-stream-2.2.0.tgz", + "integrity": "sha512-ujeqbceABgwMZxEJnk2HDY2DlnUZ+9oEcb1KzTVfYHio0UE6dG71n60d8D2I4qNvleWrrXpmjpt7vZeF1LnMZQ==", + "license": "MIT", + "dependencies": { + "bl": "^4.0.3", + "end-of-stream": "^1.4.1", + "fs-constants": "^1.0.0", + "inherits": "^2.0.3", + "readable-stream": "^3.1.1" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/tunnel-agent": { + "version": "0.6.0", + "resolved": "https://registry.npmjs.org/tunnel-agent/-/tunnel-agent-0.6.0.tgz", + "integrity": "sha512-McnNiV1l8RYeY8tBgEpuodCC1mLUdbSN+CYBL7kJsJNInOP8UjDDEwdk6Mw60vdLLrr5NHKZhMAOSrR2NZuQ+w==", + "license": "Apache-2.0", + "dependencies": { + "safe-buffer": "^5.0.1" + }, + "engines": { + "node": "*" + } + }, + "node_modules/typescript": { + "version": "5.9.3", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.9.3.tgz", + "integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==", + "dev": true, + "license": "Apache-2.0", + "bin": { + "tsc": "bin/tsc", + "tsserver": "bin/tsserver" + }, + "engines": { + "node": ">=14.17" + } + }, + "node_modules/undici-types": { + "version": "6.21.0", + "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.21.0.tgz", + "integrity": "sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/util-deprecate": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz", + "integrity": "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==", + "license": "MIT" + }, + "node_modules/wrappy": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", + "integrity": "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==", + "license": "ISC" + } + } +} diff --git a/package.json b/package.json new file mode 100644 index 0000000..c7e2ed1 --- /dev/null +++ b/package.json @@ -0,0 +1,19 @@ +{ + "name": "canvas-hello-world", + "version": "1.0.0", + "description": "A Hello World app using Canvas and TypeScript", + "main": "dist/index.js", + "scripts": { + "build": "tsc", + "start": "node dist/index.js", + "dev": "tsc && node dist/index.js", + "watch": "tsc --watch" + }, + "devDependencies": { + "typescript": "^5.0.0", + "@types/node": "^20.0.0" + }, + "dependencies": { + "canvas": "^3.2.0" + } +} diff --git a/replay_structure.md b/replay_structure.md new file mode 100644 index 0000000..c7cac72 --- /dev/null +++ b/replay_structure.md @@ -0,0 +1,105 @@ +# JSON Structure Analysis + +## Root Object Structure +The JSON file contains a single root object with the following structure: + +```json +{ + "id": "string", + "level": "string", + "myTeam": "string", + "myUsername": "string", + "players": [ + // Array of player objects + ] +} +``` + +## Player Object Structure +Each player in the `players` array has the following structure: + +```json +{ + "username": "string", + "isLocalPlayer": "boolean", + "teamId": "string", + "planeVarient": "number", + "skinVarient": "number", + "color": { + "r": "number", + "g": "number", + "b": "number", + "a": "number" + }, + "snapshots": [ + // Array of snapshot objects + ] +} +``` + +## Snapshot Object Structure +Each snapshot in the `snapshots` array has the following structure: + +```json +{ + "time": "number", + "position": { + "x": "number", + "y": "number", + "z": "number" + }, + "rotation": { + "x": "number", + "y": "number", + "z": "number", + "w": "number" + }, + "health": "number", + "throttle": "number", + "isShooting": "boolean" +} +``` + +## Complete Structure Summary + +``` +Root Object +├── id (string) +├── level (string) +├── myTeam (string) +├── myUsername (string) +└── players (array) + └── Player Object + ├── username (string) + ├── isLocalPlayer (boolean) + ├── teamId (string) + ├── planeVarient (number) + ├── skinVarient (number) + ├── color (object) + │ ├── r (number) + │ ├── g (number) + │ ├── b (number) + │ └── a (number) + └── snapshots (array) + └── Snapshot Object + ├── time (number) + ├── position (object) + │ ├── x (number) + │ ├── y (number) + │ └── z (number) + ├── rotation (object) + │ ├── x (number) + │ ├── y (number) + │ ├── z (number) + │ └── w (number) + ├── health (number) + ├── throttle (number) + └── isShooting (boolean) +``` + +## Data Type Notes +- All numeric values appear to be floating-point numbers +- The `rotation` object uses quaternion representation (x, y, z, w) +- The `color` object uses RGBA values (0.0 to 1.0 range) +- The `position` object represents 3D coordinates +- The `snapshots` array contains time-series data for player movement and state diff --git a/src/index.ts b/src/index.ts new file mode 100644 index 0000000..0bfaadb --- /dev/null +++ b/src/index.ts @@ -0,0 +1,76 @@ +import { createCanvas } from "canvas"; +import { readReplay } from "./utils/replay_reader"; +import { drawTimeBasedHeatmap, getReplayDuration } from "./utils/drawer"; + +const replayPath = "/home/warlock/.var/app/com.unity.UnityHub/config/unity3d/Milk Carton Games/Super Cloudfight/Replays/"; + +const fs = require("fs"); + +// Get all JSON file paths from replayPath +const path = require("path"); +const replayFiles = fs.readdirSync(replayPath) + .filter((file: string) => file.endsWith(".json")) + .map((file: string) => path.join(replayPath, file)); + +replayFiles.forEach(async (filePath: string) => { + const replayData = readReplay(filePath); + const baseFileName = path.basename(filePath, '.json'); + + // Timelapse configuration + const targetFPS = 60; // Output video FPS + const timelapseSpeed = 5; // 5x, 10x, etc. + const framesPerSecond = targetFPS / timelapseSpeed; // Frames to generate per second of real-time + + // Get replay duration in seconds + const duration = getReplayDuration(replayData); + console.log(`Processing ${baseFileName}: ${duration} seconds (${framesPerSecond} fps @ ${timelapseSpeed}x speed)`); + + // Create main directory for this replay + const replayDir = baseFileName; + if (!fs.existsSync(replayDir)) { + fs.mkdirSync(replayDir); + } + + // Create subdirectories for each view + const views: Array<{mode: 'top' | 'side' | 'front', dirName: string}> = [ + { mode: 'top', dirName: 'top' }, + { mode: 'side', dirName: 'side' }, + { mode: 'front', dirName: 'front' } + ]; + + for (const { mode, dirName } of views) { + const viewDir = path.join(replayDir, dirName); + if (!fs.existsSync(viewDir)) { + fs.mkdirSync(viewDir); + } + + // Get background image path + const backgroundImagePath = path.join(__dirname, '..', 'maps', 'hurricane', `${mode}.png`); + + // Calculate total number of frames to generate + const totalFrames = Math.ceil(duration * framesPerSecond); + + // Generate heatmaps at the calculated frame rate + for (let frameNumber = 0; frameNumber < totalFrames; frameNumber++) { + // Calculate the time point for this frame + const timePoint = frameNumber / framesPerSecond; + + const canvas = createCanvas(1024, 1024); + + // Generate time-based heatmap + await drawTimeBasedHeatmap(canvas, replayData, timePoint, { + width: 200, // Heatmap resolution + height: 200, + worldSize: 1024, // World size in units + sensitivity: 0.7, // Controls sensitivity to lower values (0.1-2.0) + blurAmount: 3, // Controls smoothness of heat areas (1-5) + viewMode: mode, // View mode: 'top', 'side', or 'front' + backgroundImage: backgroundImagePath + }); + + const outputPath = path.join(viewDir, `${(frameNumber + 1).toString().padStart(3, '0')}.png`); + fs.writeFileSync(outputPath, canvas.toBuffer("image/png")); + console.log(`${outputPath} (${mode} view, frame ${frameNumber + 1}/${totalFrames}, t=${timePoint.toFixed(2)}s)`); + } + } +}); diff --git a/src/types/replay.ts b/src/types/replay.ts new file mode 100644 index 0000000..69d7004 --- /dev/null +++ b/src/types/replay.ts @@ -0,0 +1,47 @@ +// TypeScript interfaces for the replay data structure +export interface Position { + x: number; + y: number; + z: number; +} + +export interface Rotation { + x: number; + y: number; + z: number; + w: number; +} + +export interface Color { + r: number; + g: number; + b: number; + a: number; +} + +export interface Snapshot { + time: number; + position: Position; + rotation: Rotation; + health: number; + throttle: number; + isShooting: boolean; +} + +export interface Player { + username: string; + isLocalPlayer: boolean; + teamId: string; + planeVarient: number; + skinVarient: number; + color: Color; + snapshots: Snapshot[]; +} + +export interface ReplayData { + id: string; + level: string; + myTeam: string; + myUsername: string; + players: Player[]; +} \ No newline at end of file diff --git a/src/utils/drawer.ts b/src/utils/drawer.ts new file mode 100644 index 0000000..f407dd2 --- /dev/null +++ b/src/utils/drawer.ts @@ -0,0 +1,291 @@ +import { Canvas, loadImage } from "canvas"; +import { ReplayData } from "../types/replay"; +import * as path from "path"; + +type ViewMode = 'top' | 'side' | 'front'; + +interface HeatmapConfig { + width: number; + height: number; + worldSize: number; + sensitivity: number; + blurAmount: number; + viewMode: ViewMode; + backgroundImage?: string; +} + +const defaultConfig: HeatmapConfig = { + width: 100, + height: 100, + worldSize: 500, + sensitivity: 0.7, + blurAmount: 3, + viewMode: 'top' +}; + +export const drawHeatmap = (canvas: Canvas, replayData: ReplayData, config: Partial = {}) => { + const finalConfig = { ...defaultConfig, ...config }; + + // Initialize heatmap array + const heatmap: number[][] = Array(finalConfig.height).fill(null).map(() => Array(finalConfig.width).fill(0)); + + let good = 0, bad = 0; + + // Process all player positions + for (const player of replayData.players) { + for (const snapshot of player.snapshots) { + const pos = snapshot.position; + + // Convert world coordinates to heatmap coordinates based on view mode + let x: number, y: number; + + switch (finalConfig.viewMode) { + case 'top': + // Top view: X,Z -> heatmap X,Y + x = Math.floor((pos.x / finalConfig.worldSize + 0.5) * finalConfig.width); + y = Math.floor((pos.z / finalConfig.worldSize + 0.5) * finalConfig.height); + break; + case 'side': + // Side view: Z,Y -> heatmap X,Y + x = Math.floor((pos.z / finalConfig.worldSize + 0.5) * finalConfig.width); + y = Math.floor((pos.y / finalConfig.worldSize + 0.5) * finalConfig.height); + break; + case 'front': + // Front view: X,Y -> heatmap X,Y + x = Math.floor((pos.x / finalConfig.worldSize + 0.5) * finalConfig.width); + y = Math.floor((pos.y / finalConfig.worldSize + 0.5) * finalConfig.height); + break; + default: + throw new Error(`Unknown view mode: ${finalConfig.viewMode}`); + } + + if (x >= 0 && x < finalConfig.width && y >= 0 && y < finalConfig.height) { + // Apply blur effect for smoother heatmap areas + const blurRadius = finalConfig.blurAmount; + for (let dx = -blurRadius; dx <= blurRadius; dx++) { + for (let dy = -blurRadius; dy <= blurRadius; dy++) { + const nx = x + dx; + const ny = y + dy; + + if (nx >= 0 && nx < finalConfig.width && ny >= 0 && ny < finalConfig.height) { + const distance = Math.sqrt(dx * dx + dy * dy); + // Gaussian-like falloff + const weight = Math.exp(-distance * distance / (2 * blurRadius * blurRadius)); + heatmap[ny][nx] += Math.round(weight * 5); + } + } + } + good++; + } else { + bad++; + } + } + } + + console.log(`Good: ${good}, Bad: ${bad}`); + + // Find maximum count for normalization + let maxCount = 0; + for (let y = 0; y < finalConfig.height; y++) { + for (let x = 0; x < finalConfig.width; x++) { + maxCount = Math.max(maxCount, heatmap[y][x]); + } + } + + // Create image data + const ctx = canvas.getContext("2d"); + const imageData = ctx.createImageData(finalConfig.width, finalConfig.height); + + // Generate heatmap colors + for (let y = 0; y < finalConfig.height; y++) { + for (let x = 0; x < finalConfig.width; x++) { + let value = heatmap[y][x] / maxCount; + + // Apply power curve to increase sensitivity to lower values + value = Math.pow(value, finalConfig.sensitivity); + + const color = getHeatmapColor(value); + const index = (y * finalConfig.width + x) * 4; + + imageData.data[index] = color.r; // Red + imageData.data[index + 1] = color.g; // Green + imageData.data[index + 2] = color.b; // Blue + imageData.data[index + 3] = 255; // Alpha + } + } + + // Scale the image to canvas size + const tempCanvas = canvas.constructor as any; + const tempCtx = new tempCanvas(finalConfig.width, finalConfig.height).getContext("2d"); + tempCtx.putImageData(imageData, 0, 0); + + // Clear main canvas and draw scaled heatmap + ctx.fillStyle = "white"; + ctx.fillRect(0, 0, canvas.width, canvas.height); + ctx.drawImage(tempCtx.canvas, 0, 0, canvas.width, canvas.height); +}; + +function getHeatmapColor(value: number): { r: number; g: number; b: number } { + // Blue -> Green -> Yellow -> Red gradient + if (value < 0.33) { + // Blue to Green (0.0 to 0.33) + const t = value / 0.33; + return { + r: Math.round(lerp(0, 0, t) * 255), + g: Math.round(lerp(0, 1, t) * 255), + b: Math.round(lerp(1, 0, t) * 255) + }; + } else if (value < 0.66) { + // Green to Yellow (0.33 to 0.66) + const t = (value - 0.33) / 0.33; + return { + r: Math.round(lerp(0, 1, t) * 255), + g: Math.round(lerp(1, 1, t) * 255), + b: Math.round(lerp(0, 0, t) * 255) + }; + } else { + // Yellow to Red (0.66 to 1.0) + const t = (value - 0.66) / 0.34; + return { + r: Math.round(lerp(1, 1, t) * 255), + g: Math.round(lerp(1, 0, t) * 255), + b: Math.round(lerp(0, 0, t) * 255) + }; + } +} + +function lerp(a: number, b: number, t: number): number { + return a + (b - a) * t; +} + +export const drawTimeBasedHeatmap = async (canvas: Canvas, replayData: ReplayData, maxTime: number, config: Partial = {}) => { + const finalConfig = { ...defaultConfig, ...config }; + + // Initialize heatmap array + const heatmap: number[][] = Array(finalConfig.height).fill(null).map(() => Array(finalConfig.width).fill(0)); + + let good = 0, bad = 0; + + // Process all player positions up to maxTime + for (const player of replayData.players) { + for (const snapshot of player.snapshots) { + // Only include positions up to the specified time + if (snapshot.time > maxTime) { + continue; + } + + const pos = snapshot.position; + + // Convert world coordinates to heatmap coordinates based on view mode + let x: number, y: number; + + switch (finalConfig.viewMode) { + case 'top': + // Top view: X,Z -> heatmap X,Y + x = Math.floor((pos.x / finalConfig.worldSize + 0.5) * finalConfig.width); + y = Math.floor((pos.z / finalConfig.worldSize + 0.5) * finalConfig.height); + break; + case 'side': + // Side view: Z,Y -> heatmap X,Y + x = Math.floor((pos.z / finalConfig.worldSize + 0.5) * finalConfig.width); + y = Math.floor((pos.y / finalConfig.worldSize + 0.5) * finalConfig.height); + break; + case 'front': + // Front view: X,Y -> heatmap X,Y + x = Math.floor((pos.x / finalConfig.worldSize + 0.5) * finalConfig.width); + y = Math.floor((pos.y / finalConfig.worldSize + 0.5) * finalConfig.height); + break; + default: + throw new Error(`Unknown view mode: ${finalConfig.viewMode}`); + } + + if (x >= 0 && x < finalConfig.width && y >= 0 && y < finalConfig.height) { + // Apply blur effect for smoother heatmap areas + const blurRadius = finalConfig.blurAmount; + for (let dx = -blurRadius; dx <= blurRadius; dx++) { + for (let dy = -blurRadius; dy <= blurRadius; dy++) { + const nx = x + dx; + const ny = y + dy; + + if (nx >= 0 && nx < finalConfig.width && ny >= 0 && ny < finalConfig.height) { + const distance = Math.sqrt(dx * dx + dy * dy); + // Gaussian-like falloff + const weight = Math.exp(-distance * distance / (2 * blurRadius * blurRadius)); + heatmap[ny][nx] += Math.round(weight * 5); + } + } + } + good++; + } else { + bad++; + } + } + } + + console.log(`Time ${maxTime}s - Good: ${good}, Bad: ${bad}`); + + // Find maximum count for normalization + let maxCount = 0; + for (let y = 0; y < finalConfig.height; y++) { + for (let x = 0; x < finalConfig.width; x++) { + maxCount = Math.max(maxCount, heatmap[y][x]); + } + } + + // Create image data + const ctx = canvas.getContext("2d"); + const imageData = ctx.createImageData(finalConfig.width, finalConfig.height); + + // Generate heatmap colors with transparency + for (let y = 0; y < finalConfig.height; y++) { + for (let x = 0; x < finalConfig.width; x++) { + let value = maxCount > 0 ? heatmap[y][x] / maxCount : 0; + + // Apply power curve to increase sensitivity to lower values + value = Math.pow(value, finalConfig.sensitivity); + + const color = getHeatmapColor(value); + const index = (y * finalConfig.width + x) * 4; + + imageData.data[index] = color.r; // Red + imageData.data[index + 1] = color.g; // Green + imageData.data[index + 2] = color.b; // Blue + // Make heatmap semi-transparent based on intensity + imageData.data[index + 3] = value > 0 ? Math.round(value * 200 + 55) : 0; // Alpha (0-255) + } + } + + // Scale the heatmap to a temporary canvas + const tempCanvas = canvas.constructor as any; + const tempCtx = new tempCanvas(finalConfig.width, finalConfig.height).getContext("2d"); + tempCtx.putImageData(imageData, 0, 0); + + // Clear main canvas + ctx.fillStyle = "white"; + ctx.fillRect(0, 0, canvas.width, canvas.height); + + // Draw background image if provided + if (finalConfig.backgroundImage) { + try { + const bgImage = await loadImage(finalConfig.backgroundImage); + ctx.drawImage(bgImage, 0, 0, canvas.width, canvas.height); + } catch (error) { + console.error(`Failed to load background image: ${finalConfig.backgroundImage}`, error); + } + } + + // Draw scaled heatmap on top with transparency + ctx.drawImage(tempCtx.canvas, 0, 0, canvas.width, canvas.height); +}; + +export const getReplayDuration = (replayData: ReplayData): number => { + let maxTime = 0; + + for (const player of replayData.players) { + for (const snapshot of player.snapshots) { + maxTime = Math.max(maxTime, snapshot.time); + } + } + + return Math.ceil(maxTime); // Round up to get total seconds +}; \ No newline at end of file diff --git a/src/utils/replay_reader.ts b/src/utils/replay_reader.ts new file mode 100644 index 0000000..8411c4c --- /dev/null +++ b/src/utils/replay_reader.ts @@ -0,0 +1,23 @@ +import * as fs from 'fs'; +import { ReplayData, Position } from '../types/replay'; + +export const readReplay = (path: string): ReplayData => { + const replay = fs.readFileSync(path, "utf8"); + return JSON.parse(replay); +} + +export const collectAllPositions = (path: string): Position[] => { + const replayData = readReplay(path); + const positions: Position[] = []; + + // Iterate through all players + for (const player of replayData.players) { + // Iterate through all snapshots for each player + for (const snapshot of player.snapshots) { + // Add the position to our collection (keeping duplicates) + positions.push(snapshot.position); + } + } + + return positions; +} \ No newline at end of file diff --git a/tsconfig.json b/tsconfig.json new file mode 100644 index 0000000..7e1fcf6 --- /dev/null +++ b/tsconfig.json @@ -0,0 +1,23 @@ +{ + "compilerOptions": { + "target": "ES2020", + "module": "commonjs", + "outDir": "./dist", + "rootDir": "./src", + "strict": true, + "esModuleInterop": true, + "skipLibCheck": true, + "forceConsistentCasingInFileNames": true, + "resolveJsonModule": true, + "declaration": true, + "declarationMap": true, + "sourceMap": true + }, + "include": [ + "src/**/*" + ], + "exclude": [ + "node_modules", + "dist" + ] +}