init
This commit is contained in:
commit
c8feceedca
66
.gitignore
vendored
Normal file
66
.gitignore
vendored
Normal file
|
|
@ -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
|
||||||
|
|
||||||
125
2025-10-06_18-00-42/create_4panel_video.py
Executable file
125
2025-10-06_18-00-42/create_4panel_video.py
Executable file
|
|
@ -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()
|
||||||
96
2025-10-06_18-00-42/create_4panel_video.sh
Executable file
96
2025-10-06_18-00-42/create_4panel_video.sh
Executable file
|
|
@ -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"
|
||||||
BIN
2025-10-06_18-00-42/temp_frames/output.mp4
Normal file
BIN
2025-10-06_18-00-42/temp_frames/output.mp4
Normal file
Binary file not shown.
492
package-lock.json
generated
Normal file
492
package-lock.json
generated
Normal file
|
|
@ -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"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
19
package.json
Normal file
19
package.json
Normal file
|
|
@ -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"
|
||||||
|
}
|
||||||
|
}
|
||||||
105
replay_structure.md
Normal file
105
replay_structure.md
Normal file
|
|
@ -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
|
||||||
76
src/index.ts
Normal file
76
src/index.ts
Normal file
|
|
@ -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)`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
47
src/types/replay.ts
Normal file
47
src/types/replay.ts
Normal file
|
|
@ -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[];
|
||||||
|
}
|
||||||
291
src/utils/drawer.ts
Normal file
291
src/utils/drawer.ts
Normal file
|
|
@ -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<HeatmapConfig> = {}) => {
|
||||||
|
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<HeatmapConfig> = {}) => {
|
||||||
|
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
|
||||||
|
};
|
||||||
23
src/utils/replay_reader.ts
Normal file
23
src/utils/replay_reader.ts
Normal file
|
|
@ -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;
|
||||||
|
}
|
||||||
23
tsconfig.json
Normal file
23
tsconfig.json
Normal file
|
|
@ -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"
|
||||||
|
]
|
||||||
|
}
|
||||||
Loading…
Reference in New Issue
Block a user