Skip to content

Instantly share code, notes, and snippets.

@lemanschik
Created September 2, 2025 06:25
Show Gist options
  • Select an option

  • Save lemanschik/df7253b720e8c583db04fe982c734b18 to your computer and use it in GitHub Desktop.

Select an option

Save lemanschik/df7253b720e8c583db04fe982c734b18 to your computer and use it in GitHub Desktop.
Example vallheim

Of course! This is a fantastic project. Here is a complete, self-contained HTML file that includes the CSS and JavaScript to do exactly what you want.

You can save this code as a single visualizer.html file and open it in your web browser. It will work entirely locally; no web server is needed.

Key Features of this Example:

  • File Pickers: Allows you to upload your map image (.png, .jpg) and your server log file.
  • Log Parser: Uses a Regular Expression to find all player position entries in the log.
  • Canvas Drawing: Renders your map and draws the parsed path on top of it.
  • Interactive Calibration: Includes input fields for you to tweak the scale and map dimensions in real-time to get a perfect alignment.
  • Clear Instructions: Guides you on how to find the correct calibration values.

visualizer.html (Copy and paste all of this into one file)

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>Valheim Map Log Visualizer</title>
    <style>
        body {
            font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
            background-color: #1a1a1a;
            color: #f0f0f0;
            display: flex;
            flex-direction: column;
            align-items: center;
            padding: 20px;
            margin: 0;
        }
        .container {
            max-width: 1200px;
            width: 100%;
        }
        h1, h2 {
            text-align: center;
            color: #ffc107;
        }
        .controls {
            display: flex;
            justify-content: space-around;
            flex-wrap: wrap;
            background-color: #2a2a2a;
            padding: 20px;
            border-radius: 8px;
            margin-bottom: 20px;
            box-shadow: 0 4px 8px rgba(0,0,0,0.3);
        }
        .control-group {
            padding: 15px;
            border: 1px solid #444;
            border-radius: 5px;
            margin: 10px;
        }
        .control-group h3 {
            margin-top: 0;
            border-bottom: 1px solid #ffc107;
            padding-bottom: 5px;
        }
        label {
            display: block;
            margin-bottom: 5px;
            font-weight: bold;
        }
        input[type="file"], input[type="number"] {
            padding: 8px;
            border-radius: 4px;
            border: 1px solid #555;
            background-color: #333;
            color: #f0f0f0;
            margin-top: 5px;
        }
        #status {
            text-align: center;
            margin: 15px 0;
            font-style: italic;
            color: #aaa;
        }
        #mapCanvas {
            display: block;
            margin: 0 auto;
            max-width: 100%;
            height: auto;
            background-color: #222;
            border: 2px dashed #555;
        }
        .instructions {
            background-color: #2a2a2a;
            padding: 15px;
            border-radius: 8px;
            margin-bottom: 20px;
            border-left: 4px solid #ffc107;
        }
    </style>
</head>
<body>

    <div class="container">
        <h1>Valheim Map Log Visualizer</h1>

        <div class="instructions">
            <h3>How to Use:</h3>
            <ol>
                <li><strong>Calibrate:</strong> Get your map image (often found in `C:\Users\[YourUser]\AppData\LocalLow\IronGate\Valheim\worlds_local\`). Open it in an image editor to find its dimensions (e.g., 2048x2048) and enter them below. The default scale is a good starting point.</li>
                <li><strong>Upload Map:</strong> Click "Choose File" and select your map image.</li>
                <li><strong>Upload Log:</strong> Select your server log file (e.g., `server_log.txt`). The path will be drawn automatically.</li>
                <li><strong>Fine-Tune:</strong> Adjust the "World to Pixel Scale" value until the path lines up perfectly with roads or landmarks on your map.</li>
            </ol>
        </div>

        <div class="controls">
            <div class="control-group">
                <h3>1. Calibration</h3>
                <label for="mapWidth">Map Image Width (px):</label>
                <input type="number" id="mapWidth" value="2048">

                <label for="mapHeight" style="margin-top: 10px;">Map Image Height (px):</label>
                <input type="number" id="mapHeight" value="2048">
                
                <label for="scale" style="margin-top: 10px;">World to Pixel Scale:</label>
                <input type="number" id="scale" value="0.098" step="0.0001">
            </div>

            <div class="control-group">
                <h3>2. Upload Map</h3>
                <label for="mapInput">Select Map Image (.png, .jpg):</label>
                <input type="file" id="mapInput" accept="image/png, image/jpeg">
            </div>

            <div class="control-group">
                <h3>3. Upload Log</h3>
                <label for="logInput">Select Server Log File (.log, .txt):</label>
                <input type="file" id="logInput" accept=".log, .txt">
            </div>
        </div>

        <p id="status">Waiting for map and log files...</p>
        
        <canvas id="mapCanvas"></canvas>
    </div>

    <script>
        const mapInput = document.getElementById('mapInput');
        const logInput = document.getElementById('logInput');
        const mapWidthInput = document.getElementById('mapWidth');
        const mapHeightInput = document.getElementById('mapHeight');
        const scaleInput = document.getElementById('scale');
        const canvas = document.getElementById('mapCanvas');
        const ctx = canvas.getContext('2d');
        const statusEl = document.getElementById('status');

        let mapImage = null;
        let pathCoordinates = [];

        // --- Event Listeners ---
        mapInput.addEventListener('change', handleMapUpload);
        logInput.addEventListener('change', handleLogUpload);
        [mapWidthInput, mapHeightInput, scaleInput].forEach(input => {
            input.addEventListener('change', drawMapAndPath);
            input.addEventListener('keyup', drawMapAndPath);
        });

        // --- File Handling ---
        function handleMapUpload(event) {
            const file = event.target.files[0];
            if (!file) return;

            const reader = new FileReader();
            reader.onload = function(e) {
                mapImage = new Image();
                mapImage.onload = function() {
                    console.log(`Map loaded: ${mapImage.width}x${mapImage.height}`);
                    statusEl.textContent = 'Map loaded. Ready for log file.';
                    // Automatically update dimension inputs if they seem default
                    if (mapWidthInput.value === '2048' && mapHeightInput.value === '2048') {
                         mapWidthInput.value = mapImage.width;
                         mapHeightInput.value = mapImage.height;
                    }
                    drawMapAndPath();
                }
                mapImage.src = e.target.result;
            }
            reader.readAsDataURL(file);
        }

        function handleLogUpload(event) {
            const file = event.target.files[0];
            if (!file) return;

            const reader = new FileReader();
            reader.onload = function(e) {
                const logText = e.target.result;
                console.log('Log file read, starting parse...');
                pathCoordinates = parseValheimLog(logText);
                statusEl.textContent = `Log parsed. Found ${pathCoordinates.length} position entries.`;
                console.log(`Parsed ${pathCoordinates.length} coordinates.`);
                drawMapAndPath();
            }
            reader.readAsText(file);
        }

        /**
         * Parses the raw text from a Valheim log file to extract player positions.
         * Looks for lines like: ZNet: Player 'PlayerName' pos=(x, y, z)
         */
        function parseValheimLog(logText) {
            // Regex to capture the X and Z coordinates from the position log entry.
            // It captures the first and third numbers inside pos=(...)
            const posRegex = /Player '.*?' pos=\(([^,]+), [^,]+, ([^)]+)\)/g;
            const coordinates = [];
            let match;

            while ((match = posRegex.exec(logText)) !== null) {
                const worldX = parseFloat(match[1]);
                const worldZ = parseFloat(match[2]); // In Valheim, Z is the North/South axis
                
                if (!isNaN(worldX) && !isNaN(worldZ)) {
                    coordinates.push({ x: worldX, z: worldZ });
                }
            }
            return coordinates;
        }

        /**
         * Converts Valheim world coordinates to pixel coordinates on the canvas.
         */
        function worldToPixel(worldX, worldZ) {
            const mapWidth = parseFloat(mapWidthInput.value);
            const mapHeight = parseFloat(mapHeightInput.value);
            const scale = parseFloat(scaleInput.value);

            if (isNaN(mapWidth) || isNaN(mapHeight) || isNaN(scale)) {
                return null;
            }

            const offsetX = mapWidth / 2;
            const offsetY = mapHeight / 2;

            // The formula to convert coordinates
            const pixelX = (worldX * scale) + offsetX;
            // The Z-axis needs to be inverted for the Y pixel coordinate
            const pixelY = (worldZ * -scale) + offsetY; 

            return { x: pixelX, y: pixelY };
        }

        /**
         * Clears the canvas and redraws the map image and the entire path.
         */
        function drawMapAndPath() {
            if (!mapImage) {
                // Clear canvas if map is removed
                ctx.clearRect(0, 0, canvas.width, canvas.height);
                return;
            }

            // Set canvas size to match the image for 1:1 pixel mapping
            canvas.width = mapImage.width;
            canvas.height = mapImage.height;

            // Draw the map image
            ctx.drawImage(mapImage, 0, 0);

            if (pathCoordinates.length < 2) {
                return; // Not enough points to draw a line
            }

            ctx.beginPath();
            ctx.strokeStyle = 'rgba(255, 69, 0, 0.85)'; // A nice bright orange/red
            ctx.lineWidth = 3;
            ctx.lineCap = 'round';
            ctx.lineJoin = 'round';

            // Move to the first point
            const startPoint = worldToPixel(pathCoordinates[0].x, pathCoordinates[0].z);
            if(startPoint) {
                ctx.moveTo(startPoint.x, startPoint.y);
            }

            // Draw lines to all subsequent points
            for (let i = 1; i < pathCoordinates.length; i++) {
                const point = worldToPixel(pathCoordinates[i].x, pathCoordinates[i].z);
                if(point) {
                    ctx.lineTo(point.x, point.y);
                }
            }

            // Render the path
            ctx.stroke();
            console.log("Path redrawn.");
        }

    </script>

</body>
</html>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment