progressive log
This commit is contained in:
parent
c8feceedca
commit
ea9a10b578
76
package-lock.json
generated
76
package-lock.json
generated
|
|
@ -8,13 +8,25 @@
|
||||||
"name": "canvas-hello-world",
|
"name": "canvas-hello-world",
|
||||||
"version": "1.0.0",
|
"version": "1.0.0",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"canvas": "^3.2.0"
|
"canvas": "^3.2.0",
|
||||||
|
"cli-progress": "^3.12.0"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
|
"@types/cli-progress": "^3.11.6",
|
||||||
"@types/node": "^20.0.0",
|
"@types/node": "^20.0.0",
|
||||||
"typescript": "^5.0.0"
|
"typescript": "^5.0.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/@types/cli-progress": {
|
||||||
|
"version": "3.11.6",
|
||||||
|
"resolved": "https://registry.npmjs.org/@types/cli-progress/-/cli-progress-3.11.6.tgz",
|
||||||
|
"integrity": "sha512-cE3+jb9WRlu+uOSAugewNpITJDt1VF8dHOopPO4IABFc3SXYL5WE/+PTz/FCdZRRfIujiWW3n3aMbv1eIGVRWA==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"@types/node": "*"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/@types/node": {
|
"node_modules/@types/node": {
|
||||||
"version": "20.19.19",
|
"version": "20.19.19",
|
||||||
"resolved": "https://registry.npmjs.org/@types/node/-/node-20.19.19.tgz",
|
"resolved": "https://registry.npmjs.org/@types/node/-/node-20.19.19.tgz",
|
||||||
|
|
@ -25,6 +37,15 @@
|
||||||
"undici-types": "~6.21.0"
|
"undici-types": "~6.21.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/ansi-regex": {
|
||||||
|
"version": "5.0.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz",
|
||||||
|
"integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==",
|
||||||
|
"license": "MIT",
|
||||||
|
"engines": {
|
||||||
|
"node": ">=8"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/base64-js": {
|
"node_modules/base64-js": {
|
||||||
"version": "1.5.1",
|
"version": "1.5.1",
|
||||||
"resolved": "https://registry.npmjs.org/base64-js/-/base64-js-1.5.1.tgz",
|
"resolved": "https://registry.npmjs.org/base64-js/-/base64-js-1.5.1.tgz",
|
||||||
|
|
@ -100,6 +121,18 @@
|
||||||
"integrity": "sha512-jJ0bqzaylmJtVnNgzTeSOs8DPavpbYgEr/b0YL8/2GO3xJEhInFmhKMUnEJQjZumK7KXGFhUy89PrsJWlakBVg==",
|
"integrity": "sha512-jJ0bqzaylmJtVnNgzTeSOs8DPavpbYgEr/b0YL8/2GO3xJEhInFmhKMUnEJQjZumK7KXGFhUy89PrsJWlakBVg==",
|
||||||
"license": "ISC"
|
"license": "ISC"
|
||||||
},
|
},
|
||||||
|
"node_modules/cli-progress": {
|
||||||
|
"version": "3.12.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/cli-progress/-/cli-progress-3.12.0.tgz",
|
||||||
|
"integrity": "sha512-tRkV3HJ1ASwm19THiiLIXLO7Im7wlTuKnvkYaTkyoAPefqjNg7W7DHKUlGRxy9vxDvbyCYQkQozvptuMkGCg8A==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"string-width": "^4.2.3"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=4"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/decompress-response": {
|
"node_modules/decompress-response": {
|
||||||
"version": "6.0.0",
|
"version": "6.0.0",
|
||||||
"resolved": "https://registry.npmjs.org/decompress-response/-/decompress-response-6.0.0.tgz",
|
"resolved": "https://registry.npmjs.org/decompress-response/-/decompress-response-6.0.0.tgz",
|
||||||
|
|
@ -133,6 +166,12 @@
|
||||||
"node": ">=8"
|
"node": ">=8"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/emoji-regex": {
|
||||||
|
"version": "8.0.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz",
|
||||||
|
"integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==",
|
||||||
|
"license": "MIT"
|
||||||
|
},
|
||||||
"node_modules/end-of-stream": {
|
"node_modules/end-of-stream": {
|
||||||
"version": "1.4.5",
|
"version": "1.4.5",
|
||||||
"resolved": "https://registry.npmjs.org/end-of-stream/-/end-of-stream-1.4.5.tgz",
|
"resolved": "https://registry.npmjs.org/end-of-stream/-/end-of-stream-1.4.5.tgz",
|
||||||
|
|
@ -195,6 +234,15 @@
|
||||||
"integrity": "sha512-JV/yugV2uzW5iMRSiZAyDtQd+nxtUnjeLt0acNdw98kKLrvuRVyB80tsREOE7yvGVgalhZ6RNXCmEHkUKBKxew==",
|
"integrity": "sha512-JV/yugV2uzW5iMRSiZAyDtQd+nxtUnjeLt0acNdw98kKLrvuRVyB80tsREOE7yvGVgalhZ6RNXCmEHkUKBKxew==",
|
||||||
"license": "ISC"
|
"license": "ISC"
|
||||||
},
|
},
|
||||||
|
"node_modules/is-fullwidth-code-point": {
|
||||||
|
"version": "3.0.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz",
|
||||||
|
"integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==",
|
||||||
|
"license": "MIT",
|
||||||
|
"engines": {
|
||||||
|
"node": ">=8"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/mimic-response": {
|
"node_modules/mimic-response": {
|
||||||
"version": "3.1.0",
|
"version": "3.1.0",
|
||||||
"resolved": "https://registry.npmjs.org/mimic-response/-/mimic-response-3.1.0.tgz",
|
"resolved": "https://registry.npmjs.org/mimic-response/-/mimic-response-3.1.0.tgz",
|
||||||
|
|
@ -406,6 +454,32 @@
|
||||||
"safe-buffer": "~5.2.0"
|
"safe-buffer": "~5.2.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/string-width": {
|
||||||
|
"version": "4.2.3",
|
||||||
|
"resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz",
|
||||||
|
"integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"emoji-regex": "^8.0.0",
|
||||||
|
"is-fullwidth-code-point": "^3.0.0",
|
||||||
|
"strip-ansi": "^6.0.1"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=8"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/strip-ansi": {
|
||||||
|
"version": "6.0.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz",
|
||||||
|
"integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"ansi-regex": "^5.0.1"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=8"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/strip-json-comments": {
|
"node_modules/strip-json-comments": {
|
||||||
"version": "2.0.1",
|
"version": "2.0.1",
|
||||||
"resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-2.0.1.tgz",
|
"resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-2.0.1.tgz",
|
||||||
|
|
|
||||||
|
|
@ -10,10 +10,12 @@
|
||||||
"watch": "tsc --watch"
|
"watch": "tsc --watch"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"typescript": "^5.0.0",
|
"@types/cli-progress": "^3.11.6",
|
||||||
"@types/node": "^20.0.0"
|
"@types/node": "^20.0.0",
|
||||||
|
"typescript": "^5.0.0"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"canvas": "^3.2.0"
|
"canvas": "^3.2.0",
|
||||||
|
"cli-progress": "^3.12.0"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
75
src/index.ts
75
src/index.ts
|
|
@ -1,7 +1,7 @@
|
||||||
import { createCanvas } from "canvas";
|
import { createCanvas, loadImage } from "canvas";
|
||||||
import { readReplay } from "./utils/replay_reader";
|
import { readReplay } from "./utils/replay_reader";
|
||||||
import { drawTimeBasedHeatmap, getReplayDuration } from "./utils/drawer";
|
import { drawTimeBasedHeatmap, getReplayDuration } from "./utils/drawer";
|
||||||
|
import * as cliProgress from "cli-progress";
|
||||||
const replayPath = "/home/warlock/.var/app/com.unity.UnityHub/config/unity3d/Milk Carton Games/Super Cloudfight/Replays/";
|
const replayPath = "/home/warlock/.var/app/com.unity.UnityHub/config/unity3d/Milk Carton Games/Super Cloudfight/Replays/";
|
||||||
|
|
||||||
const fs = require("fs");
|
const fs = require("fs");
|
||||||
|
|
@ -12,7 +12,14 @@ const replayFiles = fs.readdirSync(replayPath)
|
||||||
.filter((file: string) => file.endsWith(".json"))
|
.filter((file: string) => file.endsWith(".json"))
|
||||||
.map((file: string) => path.join(replayPath, file));
|
.map((file: string) => path.join(replayPath, file));
|
||||||
|
|
||||||
replayFiles.forEach(async (filePath: string) => {
|
// Create multibar container
|
||||||
|
const multibar = new cliProgress.MultiBar({
|
||||||
|
clearOnComplete: false,
|
||||||
|
hideCursor: true,
|
||||||
|
format: '{bar} | {filename} | {view} | {value}/{total} frames | {percentage}%'
|
||||||
|
}, cliProgress.Presets.shades_classic);
|
||||||
|
|
||||||
|
async function processReplayFile(filePath: string) {
|
||||||
const replayData = readReplay(filePath);
|
const replayData = readReplay(filePath);
|
||||||
const baseFileName = path.basename(filePath, '.json');
|
const baseFileName = path.basename(filePath, '.json');
|
||||||
|
|
||||||
|
|
@ -23,7 +30,6 @@ replayFiles.forEach(async (filePath: string) => {
|
||||||
|
|
||||||
// Get replay duration in seconds
|
// Get replay duration in seconds
|
||||||
const duration = getReplayDuration(replayData);
|
const duration = getReplayDuration(replayData);
|
||||||
console.log(`Processing ${baseFileName}: ${duration} seconds (${framesPerSecond} fps @ ${timelapseSpeed}x speed)`);
|
|
||||||
|
|
||||||
// Create main directory for this replay
|
// Create main directory for this replay
|
||||||
const replayDir = baseFileName;
|
const replayDir = baseFileName;
|
||||||
|
|
@ -38,26 +44,45 @@ replayFiles.forEach(async (filePath: string) => {
|
||||||
{ mode: 'front', dirName: 'front' }
|
{ mode: 'front', dirName: 'front' }
|
||||||
];
|
];
|
||||||
|
|
||||||
|
// Calculate total number of frames to generate
|
||||||
|
const totalFrames = Math.ceil(duration * framesPerSecond);
|
||||||
|
|
||||||
|
// Create progress bars for each view
|
||||||
|
const progressBars = new Map<string, cliProgress.SingleBar>();
|
||||||
|
for (const { mode } of views) {
|
||||||
|
const bar = multibar.create(totalFrames, 0, {
|
||||||
|
filename: baseFileName,
|
||||||
|
view: mode.padEnd(5, ' ')
|
||||||
|
});
|
||||||
|
progressBars.set(mode, bar);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Reuse a single canvas for all frames to reduce memory allocation
|
||||||
|
const canvas = createCanvas(1024, 1024);
|
||||||
|
|
||||||
for (const { mode, dirName } of views) {
|
for (const { mode, dirName } of views) {
|
||||||
const viewDir = path.join(replayDir, dirName);
|
const viewDir = path.join(replayDir, dirName);
|
||||||
if (!fs.existsSync(viewDir)) {
|
if (!fs.existsSync(viewDir)) {
|
||||||
fs.mkdirSync(viewDir);
|
fs.mkdirSync(viewDir);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Get background image path
|
// Get background image path and preload it once
|
||||||
const backgroundImagePath = path.join(__dirname, '..', 'maps', 'hurricane', `${mode}.png`);
|
const backgroundImagePath = path.join(__dirname, '..', 'maps', 'hurricane', `${mode}.png`);
|
||||||
|
let preloadedBackground;
|
||||||
|
try {
|
||||||
|
preloadedBackground = await loadImage(backgroundImagePath);
|
||||||
|
} catch (error) {
|
||||||
|
console.error(`Failed to preload background image: ${backgroundImagePath}`, error);
|
||||||
|
}
|
||||||
|
|
||||||
// Calculate total number of frames to generate
|
const progressBar = progressBars.get(mode)!;
|
||||||
const totalFrames = Math.ceil(duration * framesPerSecond);
|
|
||||||
|
|
||||||
// Generate heatmaps at the calculated frame rate
|
// Generate heatmaps at the calculated frame rate
|
||||||
for (let frameNumber = 0; frameNumber < totalFrames; frameNumber++) {
|
for (let frameNumber = 0; frameNumber < totalFrames; frameNumber++) {
|
||||||
// Calculate the time point for this frame
|
// Calculate the time point for this frame
|
||||||
const timePoint = frameNumber / framesPerSecond;
|
const timePoint = frameNumber / framesPerSecond;
|
||||||
|
|
||||||
const canvas = createCanvas(1024, 1024);
|
// Generate time-based heatmap (reusing the same canvas)
|
||||||
|
|
||||||
// Generate time-based heatmap
|
|
||||||
await drawTimeBasedHeatmap(canvas, replayData, timePoint, {
|
await drawTimeBasedHeatmap(canvas, replayData, timePoint, {
|
||||||
width: 200, // Heatmap resolution
|
width: 200, // Heatmap resolution
|
||||||
height: 200,
|
height: 200,
|
||||||
|
|
@ -65,12 +90,36 @@ replayFiles.forEach(async (filePath: string) => {
|
||||||
sensitivity: 0.7, // Controls sensitivity to lower values (0.1-2.0)
|
sensitivity: 0.7, // Controls sensitivity to lower values (0.1-2.0)
|
||||||
blurAmount: 3, // Controls smoothness of heat areas (1-5)
|
blurAmount: 3, // Controls smoothness of heat areas (1-5)
|
||||||
viewMode: mode, // View mode: 'top', 'side', or 'front'
|
viewMode: mode, // View mode: 'top', 'side', or 'front'
|
||||||
backgroundImage: backgroundImagePath
|
preloadedBackground: preloadedBackground
|
||||||
});
|
});
|
||||||
|
|
||||||
const outputPath = path.join(viewDir, `${(frameNumber + 1).toString().padStart(3, '0')}.png`);
|
const outputPath = path.join(viewDir, `${(frameNumber + 1).toString().padStart(3, '0')}.png`);
|
||||||
fs.writeFileSync(outputPath, canvas.toBuffer("image/png"));
|
fs.writeFileSync(outputPath, canvas.toBuffer("image/png"));
|
||||||
console.log(`${outputPath} (${mode} view, frame ${frameNumber + 1}/${totalFrames}, t=${timePoint.toFixed(2)}s)`);
|
|
||||||
|
// Update progress bar
|
||||||
|
progressBar.update(frameNumber + 1);
|
||||||
|
|
||||||
|
// Allow the event loop to process (enables real-time progress bar updates)
|
||||||
|
if (frameNumber % 5 === 0) {
|
||||||
|
await new Promise(resolve => setImmediate(resolve));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
});
|
|
||||||
|
// Force garbage collection hint after each view
|
||||||
|
if (global.gc) {
|
||||||
|
global.gc();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Process all replay files sequentially
|
||||||
|
(async () => {
|
||||||
|
for (const filePath of replayFiles) {
|
||||||
|
await processReplayFile(filePath);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Stop multibar after all processing is complete
|
||||||
|
multibar.stop();
|
||||||
|
})();
|
||||||
|
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,4 @@
|
||||||
import { Canvas, loadImage } from "canvas";
|
import { Canvas, loadImage, Image } from "canvas";
|
||||||
import { ReplayData } from "../types/replay";
|
import { ReplayData } from "../types/replay";
|
||||||
import * as path from "path";
|
import * as path from "path";
|
||||||
|
|
||||||
|
|
@ -12,6 +12,7 @@ interface HeatmapConfig {
|
||||||
blurAmount: number;
|
blurAmount: number;
|
||||||
viewMode: ViewMode;
|
viewMode: ViewMode;
|
||||||
backgroundImage?: string;
|
backgroundImage?: string;
|
||||||
|
preloadedBackground?: Image;
|
||||||
}
|
}
|
||||||
|
|
||||||
const defaultConfig: HeatmapConfig = {
|
const defaultConfig: HeatmapConfig = {
|
||||||
|
|
@ -222,7 +223,7 @@ export const drawTimeBasedHeatmap = async (canvas: Canvas, replayData: ReplayDat
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
console.log(`Time ${maxTime}s - Good: ${good}, Bad: ${bad}`);
|
// console.log(`Time ${maxTime}s - Good: ${good}, Bad: ${bad}`);
|
||||||
|
|
||||||
// Find maximum count for normalization
|
// Find maximum count for normalization
|
||||||
let maxCount = 0;
|
let maxCount = 0;
|
||||||
|
|
@ -264,8 +265,10 @@ export const drawTimeBasedHeatmap = async (canvas: Canvas, replayData: ReplayDat
|
||||||
ctx.fillStyle = "white";
|
ctx.fillStyle = "white";
|
||||||
ctx.fillRect(0, 0, canvas.width, canvas.height);
|
ctx.fillRect(0, 0, canvas.width, canvas.height);
|
||||||
|
|
||||||
// Draw background image if provided
|
// Draw background image if provided (use preloaded image if available)
|
||||||
if (finalConfig.backgroundImage) {
|
if (finalConfig.preloadedBackground) {
|
||||||
|
ctx.drawImage(finalConfig.preloadedBackground, 0, 0, canvas.width, canvas.height);
|
||||||
|
} else if (finalConfig.backgroundImage) {
|
||||||
try {
|
try {
|
||||||
const bgImage = await loadImage(finalConfig.backgroundImage);
|
const bgImage = await loadImage(finalConfig.backgroundImage);
|
||||||
ctx.drawImage(bgImage, 0, 0, canvas.width, canvas.height);
|
ctx.drawImage(bgImage, 0, 0, canvas.width, canvas.height);
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue
Block a user