您需要先安装一个扩展,例如 篡改猴、Greasemonkey 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 Userscripts ,之后才能安装此脚本。
您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey,才能安装此脚本。
您需要先安装用户脚本管理器扩展后才能安装此脚本。
Easily download chunk images from wplace.live using multi-point selection and highlighting
// ==UserScript== // @name Wplace chunk downloader // @namespace http://tampermonkey.net/ // @version 2.4.1 // @description Easily download chunk images from wplace.live using multi-point selection and highlighting // @author NotNotWaldo // @match https://wplace.live/* // @icon https://www.google.com/s2/favicons?sz=64&domain=wplace.live // @license MIT // @grant GM_setValue // @grant GM_getValue // @grant GM.setValue // @grant GM.getValue // @run-at document-end // @grant none // ==/UserScript== // Code below, feel free to read in horror (() => { // Global vals because I'm way too lazy // These variables are for handling the highlighting feature let isHightlightOn = false; let downloadingState = false; // the downloading state ensures that the highlight wont be also printed when downloading the images of chunk let highlightedChunksLinksArr = []; // array for the highlighted chunks // the coords of chunks that are selected by the points you've set let mlChunkCoords = { firstChunk: { x: null, y: null }, secondChunk: { x: null, y: null }, }; let mlPixelCoords = { firstPixel: { x: null, y: null }, secondPixel: { x: null, y: null }, }; // variables for the currently selected chunks let chunkX = null; let chunkY = null; let pixelX = null; let pixelY = null; let chunkUrl = null; // for the dragging mechanic let isPointing = false; // just a template for chunk img const chunkTemplateUrl = `https://backend.wplace.live/files/s0/tiles/`; // for the amount of downloading instances let concurrentDlInstances = 3; // yea, dont modify this tho // you can modify this here if you want to increase the max instances let maxDlInstances = 30; // dont go lower than 1. Honestly, why would you? // variables for the download bar let currImgsDownloaded = null; let totalImgsToBeDownloaded = null; // for preset deletion confirmation let dontAskPresetDelete = false; // for lazily waiting for something const sleep = (ms) => new Promise((resolve) => setTimeout(resolve, ms)); // for getting and setting persistent values let getGMValue = (key, def) => { try { if (typeof GM !== "undefined" && typeof GM.getValue === "function") return GM.getValue(key, def); if (typeof GM_getValue === "function") return Promise.resolve(GM_getValue(key, def)); } catch {} // Fallback: localStorage try { let val = localStorage.getItem("gm_" + key); return Promise.resolve(val !== null ? JSON.parse(val) : def); } catch { return Promise.resolve(def); } }; let setGMValue = (key, value) => { try { if (typeof GM !== "undefined" && typeof GM.setValue === "function") return GM.setValue(key, value); if (typeof GM_setValue === "function") return Promise.resolve(GM_setValue(key, value)); } catch {} // Fallback: localStorage try { localStorage.setItem("gm_" + key, JSON.stringify(value)); } catch {} return Promise.resolve(); }; let savedConfigs = {}; let initConfig = async () => { savedConfigs = { concurrentDlInstances, dontAskPresetDelete, ...((await getGMValue("savedConfigs")) || {}), }; if (Object.keys(savedConfigs).length === 0) { savedConfigs.concurrentDlInstances = concurrentDlInstances; savedConfigs.dontAskPresetDelete = dontAskPresetDelete; setGMValue("savedConfigs", savedConfigs); } else { ({ concurrentDlInstances, dontAskPresetDelete } = savedConfigs); } let instanceInfo = miscSection.querySelector(".instanceInfo"); instanceInfo.textContent = `Download instances: ${concurrentDlInstances}`; }; initConfig(); let multipleChunksDownloaderElem = document.createElement("div"); multipleChunksDownloaderElem.className = "mulChunksDownloader"; multipleChunksDownloaderElem.innerHTML = ` <div class="chunk-downloader"> <!-- Multiple Chunk Downloader --> <div class="mainHead section-header"> <span>Wplace Chunks Downloader</span> <button class="simple-btn">–</button> </div> <div class="mainCollapsible expanded"> <div class="infoSection section"> <div class="section-header coords"> <span class="chunkSelectedInfo">Chunk selected: X: null, Y: null</span> </div> <div class="btn-row"> <button class="downloadChunkBtn btn btn-primary">Download Chunk</button> <button class="viewChunkBtn btn">View Chunk Image</button> </div> </div> <div class="mulChunkSection section"> <div class="section-header"> <span>Multiple Chunks Downloader</span> <button class="simple-btn">+</button> </div> <div class="mulChunksSectionsCon collapsible collapsed"> <div class="section"> <div class="chunksInfo"> <div class="coords"> <span>1st X: null, Y: null</span> <span>2nd X: null, Y: null</span> </div> <div> <span class="chunkAmountText">Current amount of chunks: 0</span> </div> </div> <div class="btn-row"> <button class="firstPointBtn btn btn-primary">Set 1st Point</button> <button class="secPointBtn btn btn-primary">Set 2nd Point</button> </div> <div class="btn-row"> <button class="downloadBtn btn btn-primary">Download Chunks</button> <button class="removePointBtn btn">Remove Points</button> </div> </div> <div class="regionDownloadSection section"> <div class="section-header"> <span>Pixel Region Downloader</span> <button class="simple-btn">+</button> </div> <div class="collapsible collapsed"> <div class="pixelCoords"> <span>1st X: null, Y: null</span> <span>2nd X: null, Y: null</span> </div> <div class="btn-col"> <button class="downloadRegionBtn btn btn-primary">Download Region</button> </div> </div> </div> <div class="miscSection section"> <div class="section-header"> <span>Misc</span> <button class="simple-btn">+</button> </div> <div class="collapsible collapsed"> <div class="multipleInstance"> <span class="instanceInfo">Download instances: ${concurrentDlInstances}</span> <div class="instanceIncDecBtns"> <button class="instanceInc simple-btn">+</button> <button class="instanceDec simple-btn">–</button> </div> </div> <div class="btn-col"> <button class="highlightBtn btn">Highlight Chunks</button> </div> </div> </div> </div> </div> <div class="savesSection section"> <div class="section-header"> <span>Presets</span> <button class="simple-btn">+</button> </div> <div class="collapsible collapsed"> <div class="saveCurrPreset"> <input class="presetNameInput input-box" type="text" name="coordsName" placeholder="Preset name"></input> <div class="btn-col"> <button class="savePresetBtn btn-primary btn">Save Current Points as Preset</button> </div> </div> <div class="savedPresets"> </div> </div> </div> <!-- Manual Chunk Download --> <div class="manualChunkSection section"> <div class="section-header"> <span>Manual Chunk Downloader</span> <button class="simple-btn">+</button> </div> <div class="collapsible collapsed"> <input class="coordsInput input-box" type="text" name="chunksCoords" placeholder="firstX, firstY, secX, secY, safety"> </input> <div class="btn-row" style="grid-template-columns: 1fr;"> <button class="manualDownloadBtn btn btn-primary">Download</button> </div> </div> </div> <div class="downloadBarCon"> <div class="download-bar"> <div class="download-progress"></div> <span class="download-text">0 / 0</span> </div> </div> </div> </div> `; let style = document.createElement("style"); style.textContent = ` /* ================================ Container & Layout ================================ */ .mulChunksDownloader { position: fixed; bottom: 12px; left: 12px; top: auto; z-index: 49; } .mulChunksDownloader .chunk-downloader { width: 360px; padding: 16px; font-family: sans-serif; font-size: 14px; color: #111827; background: #fff; border: 1px solid #e5e7eb; border-radius: 16px; box-shadow: 0 4px 12px rgba(0,0,0,0.15); } /* ================================ Sections ================================ */ .mulChunksDownloader .section { margin-bottom: 16px; } .mulChunksDownloader .mainHead { margin: 0; cursor: move; } .mulChunksDownloader .infoSection { margin-top: 16px; } .mulChunksDownloader .mulChunkSection .mulChunksSectionsCon { display: flex; flex-direction: column; gap: 10px; } .mulChunksDownloader .mulChunkSection .mulChunksSectionsCon .section { margin: 0; } /* ================================ Headers ================================ */ .mulChunksDownloader .section-header { display: flex; justify-content: space-between; align-items: center; margin-bottom: 3px; font-weight: 600; } .mulChunksDownloader .savesSection .preset-header { padding: 10px 7px; font-weight: 600; cursor: pointer; } /* ================================ Collapsibles ================================ */ .mulChunksDownloader .mainCollapsible, .mulChunksDownloader .collapsible { overflow: hidden; transition: max-height 0.3s ease; } .mulChunksDownloader .mainCollapsible.collapsed, .mulChunksDownloader .collapsible.collapsed { max-height: 0; } .mulChunksDownloader .mainCollapsible.expanded { max-height: 2000px; /* large enough to fit all content */ } .mulChunksDownloader .collapsible.expanded { max-height: 1000px; } /* ================================ Text & Info ================================ */ .mulChunksDownloader .coords { width: 100%; display: flex; justify-content: space-around; align-items: center; gap: 8px; font-size: 14px; } .mulChunksDownloader .pixelCoords { display: flex; justify-content: space-around; align-items: center; gap: 8px; padding: 4px 12px; font-size: 14px; border-radius: 9999px; background: #f3f4f6; margin-top: 10px; } .mulChunksDownloader .chunksInfo { margin: 5px 0; padding: 6px 12px; display: flex; flex-direction: column; align-items: center; gap: 8px; border-radius: 10px; background: #e8e8e9; } .mulChunksDownloader .chunkAmountText { font-weight: 600; } /* ================================ Buttons ================================ */ .mulChunksDownloader .btn { padding: 6px 12px; border: 1px solid #d1d5db; border-radius: 9999px; background: #f3f4f6; color: #374151; font-size: 13px; text-align: center; cursor: pointer; } .mulChunksDownloader .btn:hover { background: #e5e7eb; } .mulChunksDownloader .btn-primary { background: #2563eb; border: none; color: #fff; } .mulChunksDownloader .btn-primary:hover { background: #1d4ed8; } .simple-btn { width: 24px; height: 24px; font-size: 14px; color: #4b5563; display: flex; align-items: center; justify-content: center; background: #f3f4f6; border: 1px solid #d1d5db; border-radius: 6px; cursor: pointer; } .simple-btn:hover { background: #e5e7eb; } .btn-primary:hover { background: #1d4ed8; } .mulChunksDownloader .del-btn { background: #fff; border: 2px solid #eb2525; color: #eb2525; } .mulChunksDownloader .del-btn:hover { background: #c02828; color: #fff; } .mulChunksDownloader button:disabled { background-color: #4b5563; color: #9ca3af; cursor: not-allowed; opacity: 0.7; } /* ================================ Button Layout ================================ */ .mulChunksDownloader .btn-row { display: grid; grid-template-columns: 1fr 1fr; gap: 8px; margin-top: 8px; } .mulChunksDownloader .btn-col { display: flex; flex-direction: column; gap: 8px; margin-top: 8px; } /* ================================ Inputs ================================ */ .mulChunksDownloader .input-box { width: 100%; padding: 6px 12px; margin-top: 8px; font-size: 0.875rem; border: 1px solid #d1d5db; border-radius: 9999px; background: #f3f4f6; outline: none; transition: border 0.2s, box-shadow 0.2s; } .mulChunksDownloader .input-box:focus { border-color: #2563eb; background: #fff; box-shadow: 0 0 0 2px rgba(37, 99, 235, 0.2); } /* ================================ Presets ================================ */ .mulChunksDownloader .savesSection .preset { border-radius: 5px; transition: 0.2s ease; } .mulChunksDownloader .savesSection .preset:hover { background: #eeeff1; } .mulChunksDownloader .savesSection .savedPresets { display: flex; flex-direction: column; gap: 5px; margin-top: 10px; } .mulChunksDownloader .savesSection .preset .preset-header { border-bottom: 1px solid #d1d5db; display: flex; justify-content: space-between; } .mulChunksDownloader .savesSection .preset .preset-info { margin-top: 5px; display: flex; flex-direction: column; gap: 5px; } .mulChunksDownloader .savesSection .preset .point-info { display: flex; flex-direction: row; justify-content: space-around; } .mulChunksDownloader .savesSection .preset .point-num { font-weight: 600; } .mulChunksDownloader .savedPresets { max-height: 200px; overflow-y: auto; padding-right: 4px; } /* Optional: style the scrollbar for better look */ .mulChunksDownloader .savedPresets::-webkit-scrollbar { width: 6px; } .mulChunksDownloader .savedPresets::-webkit-scrollbar-thumb { background: #9ca3af; /* gray thumb */ border-radius: 3px; } .mulChunksDownloader .savedPresets::-webkit-scrollbar-thumb:hover { background: #6b7280; /* darker on hover */ } .mulChunksDownloader .savedPresets::-webkit-scrollbar-track { background: transparent; } /* ================================ Download Bar ================================ */ .mulChunksDownloader .download-bar { position: relative; width: 100%; height: 24px; margin-top: 10px; background-color: #e0e0e0; border-radius: 6px; overflow: hidden; } .mulChunksDownloader .download-progress { width: 0%; height: 100%; background-color: #007bff; transition: width 0.3s ease; } .mulChunksDownloader .download-text { position: absolute; top: 0; left: 50%; font-size: 12px; font-weight: bold; color: #fff; line-height: 24px; transform: translateX(-50%); } /* ================================ Misc Section ================================ */ .mulChunksDownloader .multipleInstance { margin-top: 10px; padding: 0 20px; display: flex; justify-content: space-between; align-items: center; font-weight: bold; } .mulChunksDownloader .instanceIncDecBtns { display: flex; gap: 10px; } /* ================================ Preset Deletion Section ================================ */ .preset-delete-modal .simple-btn { width: auto; padding: 5px 7px; } .preset-delete-modal .del-btn { background: #fff; border: 2px solid #eb2525; color: #eb2525; } .preset-delete-modal .del-btn:hover { background: #c02828; color: #fff; } `; document.head.appendChild(style); document.body.appendChild(multipleChunksDownloaderElem); // === COLLAPSING MECHANISM SECTION === let collapseSection = (btn, target) => { let isCollapsed = btn.dataset.collapsed === "true"; if (!isCollapsed) { target.classList.remove("expanded"); target.classList.add("collapsed"); btn.dataset.collapsed = "true"; btn.textContent = "+"; } else { target.classList.remove("collapsed"); target.classList.add("expanded"); btn.dataset.collapsed = "false"; btn.textContent = "–"; } }; // Main collapse let mainHead = multipleChunksDownloaderElem.querySelector(".mainHead"); const mainCollapsible = multipleChunksDownloaderElem.querySelector(".mainCollapsible"); const mainCollapseBtn = multipleChunksDownloaderElem.querySelector( ".mainHead .simple-btn" ); mainCollapseBtn.dataset.collapsed = "false"; mainCollapseBtn.addEventListener("click", () => { collapseSection(mainCollapseBtn, mainCollapsible); }); // Multiple Chunks Section collapse let mulChunkSection = multipleChunksDownloaderElem.querySelector(".mulChunkSection"); let mulChunkCollapseBtn = mulChunkSection.querySelector(".simple-btn"); let mulChunkCollapsible = mulChunkSection.querySelector(".collapsible"); mulChunkCollapseBtn.dataset.collapsed = "true"; mulChunkCollapseBtn.addEventListener("click", () => { collapseSection(mulChunkCollapseBtn, mulChunkCollapsible); }); // Misc section collapse let miscSection = mulChunkSection.querySelector(".miscSection"); let miscCollapseBtn = miscSection.querySelector(".simple-btn"); let miscCollapsible = miscSection.querySelector(".collapsible"); miscCollapseBtn.dataset.collapsed = "true"; miscCollapseBtn.addEventListener("click", () => { collapseSection(miscCollapseBtn, miscCollapsible); }); // Manual Chunks Section collapse let manualChunkSection = multipleChunksDownloaderElem.querySelector( ".manualChunkSection" ); let manualCollapseBtn = manualChunkSection.querySelector(".simple-btn"); let manualCollapsible = manualChunkSection.querySelector(".collapsible"); manualCollapseBtn.dataset.collapsed = "true"; // starts collapsed manualCollapseBtn.addEventListener("click", () => { collapseSection(manualCollapseBtn, manualCollapsible); }); let regionDownloadSection = mulChunkSection.querySelector( ".regionDownloadSection" ); let rgDlSectionCollapseBtn = regionDownloadSection.querySelector(".simple-btn"); let rgDlSectionCollapsible = regionDownloadSection.querySelector(".collapsible"); rgDlSectionCollapseBtn.dataset.collapsed = "true"; rgDlSectionCollapseBtn.addEventListener("click", () => { collapseSection(rgDlSectionCollapseBtn, rgDlSectionCollapsible); }); let savesSection = multipleChunksDownloaderElem.querySelector(".savesSection"); let savesSectionCollapsible = savesSection.querySelector(".collapsible"); let savesSectionCollapseBtn = savesSection.querySelector(".simple-btn"); savesSectionCollapseBtn.dataset.collapsed = "true"; savesSectionCollapseBtn.addEventListener("click", () => { collapseSection(savesSectionCollapseBtn, savesSectionCollapsible); }); // === COLLAPSING MECHANISM SECTION END === // for collapsing preset infos let collapsePreset = (header, target) => { let isCollapsed = header.dataset.collapsed === "true"; if (!isCollapsed) { target.classList.remove("expanded"); target.classList.add("collapsed"); header.dataset.collapsed = "true"; } else { target.classList.remove("collapsed"); target.classList.add("expanded"); header.dataset.collapsed = "false"; } }; let savedPresetsCon = savesSection.querySelector(".savedPresets"); let presetIds = new Set(); let savedPresets = {}; // for creating preset element let createPresetElem = (presetId, pointsData, onDelete) => { let { firstPoint, secondPoint, name } = pointsData; if (!firstPoint || !secondPoint) return null; let preset = document.createElement("div"); preset.className = "preset"; preset.dataset.presetID = presetId; let presetHeader = document.createElement("div"); presetHeader.className = "preset-header"; presetHeader.dataset.collapsed = "true"; let title = document.createElement("span"); title.textContent = name; let deleteBtn = document.createElement("button"); deleteBtn.className = "deletePresetBtn simple-btn del-btn"; deleteBtn.textContent = "X"; presetHeader.append(title, deleteBtn); let presetCollapsible = document.createElement("div"); presetCollapsible.className = "collapsible collapsed"; let info = document.createElement("div"); info.className = "preset-info"; info.innerHTML = ` <div> <span class="point-num">First point</span> <div class="point-info"> <span>Chunk: ${firstPoint.chunk.x}, ${firstPoint.chunk.y}</span> <span>Pixel: ${firstPoint.pixel.x}, ${firstPoint.pixel.y}</span> </div> </div> <div> <span class="point-num">Second point</span> <div class="point-info"> <span>Chunk: ${secondPoint.chunk.x}, ${secondPoint.chunk.y}</span> <span>Pixel: ${secondPoint.pixel.x}, ${secondPoint.pixel.y}</span> </div> </div> `; let presetBtnsCon = document.createElement("div"); presetBtnsCon.className = firstPoint.pixel.x == null || secondPoint.pixel.x == null ? "preset-btns btn-col" : "preset-btns btn-row"; let presetDlChunksBtn = document.createElement("button"); presetDlChunksBtn.type = "button"; presetDlChunksBtn.className = "presetDlChunksBtn dlBtn btn-primary btn"; presetDlChunksBtn.textContent = "Download Chunks"; presetBtnsCon.appendChild(presetDlChunksBtn); if (presetBtnsCon.classList.contains("btn-row")) { let presetDlRegionBtn = document.createElement("button"); presetDlRegionBtn.type = "button"; presetDlRegionBtn.className = "presetDlRegionBtn dlBtn btn-primary btn"; presetDlRegionBtn.textContent = "Download Region"; presetBtnsCon.appendChild(presetDlRegionBtn); presetDlRegionBtn.addEventListener("click", () => { regionDl?.( { firstChunk: firstPoint.chunk, secondChunk: secondPoint.chunk }, { firstPixel: firstPoint.pixel, secondPixel: secondPoint.pixel }, name, false ); }); } presetCollapsible.append(info, presetBtnsCon); preset.append(presetHeader, presetCollapsible); presetHeader.addEventListener("click", () => { collapsePreset(presetHeader, presetCollapsible); }); deleteBtn.addEventListener("click", (e) => { e.stopPropagation(); onDelete?.(presetId, preset); }); presetDlChunksBtn.addEventListener("click", () => { multipleChunksDlUrl?.( { firstChunk: firstPoint.chunk, secondChunk: secondPoint.chunk }, name, false ); }); return preset; }; // For initalizing presets let initPresets = async () => { savedPresets = (await getGMValue("savedPresets")) || {}; presetIds = new Set(Object.keys(savedPresets)); Object.entries(savedPresets).forEach(([id, data]) => { let presetElem = createPresetElem(id, data, deletePreset); savedPresetsCon.appendChild(presetElem); }); }; initPresets(); // For deleting preset function showDeleteConfirm(onConfirm) { // If user disabled confirmation, just run it if (dontAskPresetDelete) { onConfirm(); return; } // Creates a modal container const modal = document.createElement("div"); modal.className = "preset-delete-modal"; modal.style.cssText = ` position: fixed; top: 0; left: 0; right: 0; bottom: 0; background: rgba(0,0,0,0.5); display: flex; justify-content: center; align-items: center; z-index: 9999; `; // Inner box const box = document.createElement("div"); box.style.cssText = ` background: #fff; padding: 16px; border-radius: 8px; box-shadow: 0 2px 10px rgba(0,0,0,0.3); min-width: 300px; text-align: center; `; box.innerHTML = ` <p>Are you sure you want to delete this preset?</p> <label style="display:flex;align-items:center;justify-content:center;margin:8px 0;gap:6px;"> <input type="checkbox" id="dontAskCheckbox"> <span>Don’t ask again</span> </label> <div style="margin-top:12px; display:flex; gap:10px; justify-content:center;"> <button id="confirmDeleteBtn" class="simple-btn del-btn">Delete</button> <button id="cancelDeleteBtn" class="simple-btn">Cancel</button> </div> `; modal.appendChild(box); document.body.appendChild(modal); // Handlers box.querySelector("#confirmDeleteBtn").onclick = () => { const dontAsk = box.querySelector("#dontAskCheckbox").checked; if (dontAsk) dontAskPresetDelete = true; onConfirm(); modal.remove(); }; box.querySelector("#cancelDeleteBtn").onclick = () => modal.remove(); } let deletePreset = (presetId, elem) => { showDeleteConfirm(() => { savedConfigs.dontAskPresetDelete = dontAskPresetDelete; setGMValue("savedConfigs", savedConfigs); delete savedPresets[presetId]; presetIds.delete(presetId); elem.remove(); setGMValue("savedPresets", savedPresets); }); }; function generateId(length = 8) { const chars = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789"; let id; do { id = Array.from( { length }, () => chars[Math.floor(Math.random() * chars.length)] ).join(""); } while (presetIds.has(id)); // regenerate if duplicate presetIds.add(id); return id; } function createBlankPreset(name = "Untitled Preset") { return { name, firstPoint: { chunk: { x: null, y: null }, pixel: { x: null, y: null }, }, secondPoint: { chunk: { x: null, y: null }, pixel: { x: null, y: null }, }, }; } // for the dragging mechanism let isDragging = false; let offsetX = 0; let offsetY = 0; mainHead.addEventListener("mousedown", (e) => { isDragging = true; // Calculates click offset inside the box const rect = multipleChunksDownloaderElem.getBoundingClientRect(); offsetX = e.clientX - rect.left; offsetY = e.clientY - rect.top; // Prevent accidental text selection e.preventDefault(); }); document.addEventListener("mousemove", (e) => { if (!isDragging) return; multipleChunksDownloaderElem.style.top = `${e.clientY - offsetY}px`; multipleChunksDownloaderElem.style.left = `${e.clientX - offsetX}px`; multipleChunksDownloaderElem.style.bottom = "auto"; // stop sticking to bottom multipleChunksDownloaderElem.style.right = "auto"; // stop sticking to left multipleChunksDownloaderElem.style.position = "fixed"; }); document.addEventListener("mouseup", () => { isDragging = false; }); // for displaying info about points and currently selected chunk let infoSection = multipleChunksDownloaderElem.querySelector(".infoSection"); let downloadChunkBtn = infoSection.querySelector(".downloadChunkBtn"); downloadChunkBtn.addEventListener("click", async () => { if (chunkX == null) return; multipleChunksDlUrl({ firstChunk: { x: chunkX, y: chunkY }, secondChunk: { x: chunkX, y: chunkY }, }); }); let viewChunkBtn = infoSection.querySelector(".viewChunkBtn"); viewChunkBtn.addEventListener("click", (event) => { if (chunkX == null) return; window.open(chunkUrl, "_blank"); }); // to update the infos displayed const refreshSetPointsInfo = () => { let coordsCon = mulChunkSection.querySelector(".coords"); let currentCoords = infoSection.querySelector("span"); currentCoords.textContent = `Chunk selected: X: ${chunkX}, Y: ${chunkY}`; let infoChildren = coordsCon.querySelectorAll("span"); infoChildren[0].textContent = `1st X: ${mlChunkCoords.firstChunk.x}, Y: ${mlChunkCoords.firstChunk.y}`; infoChildren[1].textContent = `2nd X: ${mlChunkCoords.secondChunk.x}, Y: ${mlChunkCoords.secondChunk.y}`; let chunkAmountText = mulChunkSection.querySelector(".chunkAmountText"); let currentChunkAmount = getAmountOfChunksSelected(mlChunkCoords); chunkAmountText.textContent = `Current amount of chunks: ${currentChunkAmount}`; let pixelCoordsCon = regionDownloadSection.querySelector(".pixelCoords"); let pixelinfoChildren = pixelCoordsCon.querySelectorAll("span"); pixelinfoChildren[0].textContent = `1st X: ${mlPixelCoords.firstPixel.x}, Y: ${mlPixelCoords.firstPixel.y}`; pixelinfoChildren[1].textContent = `2nd X: ${mlPixelCoords.secondPixel.x}, Y: ${mlPixelCoords.secondPixel.y}`; }; let getAmountOfChunksSelected = (chunkCoords) => { if (chunkCoords.firstChunk.x == null && chunkCoords.secondChunk.x == null) return 0; if (chunkCoords.secondChunk.x == null || chunkCoords.firstChunk.x == null) return 1; let organizedChunkCoords = mlCoordsOrganizer(chunkCoords); let topLeft = organizedChunkCoords.firstChunk; let botRight = organizedChunkCoords.secondChunk; let width = 1 + botRight.x - topLeft.x; let height = 1 + botRight.y - topLeft.y; return width * height; }; // for the multiple chunk downloader elems/buttons let firstPointBtn = mulChunkSection.querySelector(".firstPointBtn"); let secPointBtn = mulChunkSection.querySelector(".secPointBtn"); firstPointBtn.addEventListener("click", async (e) => { await setPoint("first"); }); secPointBtn.addEventListener("click", async (e) => { await setPoint("sec"); }); let setPoint = async (position) => { if (chunkX == null) return; if (position == "first") { mlChunkCoords.firstChunk = { x: chunkX, y: chunkY }; mlPixelCoords.firstPixel = { x: pixelX, y: pixelY }; } else if (position == "sec") { mlChunkCoords.secondChunk = { x: chunkX, y: chunkY }; mlPixelCoords.secondPixel = { x: pixelX, y: pixelY }; } if (isHightlightOn) { highlightedChunksLinksArr.length = 0; let organizedCoords = await mlCoordsOrganizer(mlChunkCoords); highlightedChunksLinksArr.push( ...getLinksFromChunkCoords(organizedCoords) ); } refreshSetPointsInfo(); updateButtons(); }; let removePointsBtn = mulChunkSection.querySelector(".removePointBtn"); removePointsBtn.addEventListener("click", async () => { mlChunkCoords = { firstChunk: { x: null, y: null }, secondChunk: { x: null, y: null }, }; mlPixelCoords = { firstPixel: { x: null, y: null }, secondPixel: { x: null, y: null }, }; highlightedChunksLinksArr.length = 0; isHightlightOn = false; let highlightBtn = mulChunkSection.querySelector(".highlightBtn"); highlightBtn.textContent = "Highlight chunks"; refreshSetPointsInfo(); updateButtons(); }); // for saving coords - section let presetNameInput = savesSection.querySelector(".presetNameInput"); const invalidChars = /[\\\/:*?"<>|]/g; const maxLength = 40; presetNameInput.addEventListener("keydown", (event) => { if (event.key === "Enter" && !event.repeat) { addPreset(); } }); // block typing presetNameInput.addEventListener("keypress", (e) => { if (invalidChars.test(e.key)) e.preventDefault(); }); // sanitize pasted text presetNameInput.addEventListener("input", () => { presetNameInput.value = presetNameInput.value.replace(invalidChars, ""); if (presetNameInput.value.length > maxLength) { presetNameInput.value = presetNameInput.value.substring(0, maxLength); } }); let savePresetBtn = savesSection.querySelector(".savePresetBtn"); savePresetBtn.addEventListener("click", () => { addPreset(); }); let createPreset = async () => { let presetName = presetNameInput.value.trim() || "Untitled Preset"; let tempPreset = createBlankPreset(presetName); let presetID = generateId(); // unique random ID console.log("testing here"); // fill in values tempPreset.firstPoint.chunk = { ...mlChunkCoords.firstChunk }; tempPreset.secondPoint.chunk = { ...mlChunkCoords.secondChunk }; tempPreset.firstPoint.pixel = { ...mlPixelCoords.firstPixel }; tempPreset.secondPoint.pixel = { ...mlPixelCoords.secondPixel }; savedPresets[presetID] = structuredClone(tempPreset); await setGMValue("savedPresets", savedPresets); return { id: presetID, ...tempPreset }; }; let addPreset = async () => { if ( mlChunkCoords.firstChunk.x == null && mlChunkCoords.secondChunk.x == null ) return; if (presetIds.size >= 50) { console.warn("Maximum number of presets (50) reached"); alert("You can only save up to 50 presets."); return; } let newPreset = await createPreset(); let newPresetElem = await createPresetElem( newPreset.id, newPreset, deletePreset ); savedPresetsCon.appendChild(newPresetElem); }; let highlightBtn = miscSection.querySelector(".highlightBtn"); highlightBtn.addEventListener("click", async () => { console.log("Trying to hightlight chunks"); if (mlChunkCoords.firstChunk.x == null && mlChunkCoords.secondChunk.x) return; if (!isHightlightOn) { let organizedCoords = await mlCoordsOrganizer(mlChunkCoords); console.log(Object.keys(organizedCoords)); highlightedChunksLinksArr.push( ...getLinksFromChunkCoords(organizedCoords) ); console.log(`Turned on hightlight`); isHightlightOn = !isHightlightOn; highlightBtn.textContent = "Unhighlight chunks"; } else { highlightedChunksLinksArr.length = 0; console.log(`Turned off highlight`); isHightlightOn = !isHightlightOn; highlightBtn.textContent = "Highlight chunks"; } }); let downloadBtn = mulChunkSection.querySelector(".downloadBtn"); downloadBtn.addEventListener("click", async () => { if ( mlChunkCoords.firstChunk.x == null && mlChunkCoords.secondChunk.x == null ) { return; } multipleChunksDlUrl(mlChunkCoords, false); }); // for the region download elems/buttons let downloadRegionBtn = regionDownloadSection.querySelector(".downloadRegionBtn"); downloadRegionBtn.addEventListener("click", (event) => { console.log("Clicked region download"); if ( downloadingState || mlPixelCoords.firstPixel.x == null || mlPixelCoords.secondPixel.x == null ) return; regionDl(mlChunkCoords, mlPixelCoords, false); }); // for the manual downloading let coordsInput = manualChunkSection.querySelector(".coordsInput"); coordsInput.addEventListener("keydown", (event) => { if (event.key === "Enter" && !event.repeat) { manualDownload(); } }); let manualDownloadBtn = manualChunkSection.querySelector(".manualDownloadBtn"); manualDownloadBtn.addEventListener("click", () => { manualDownload(); }); let manualDownload = () => { if (downloadingState) return; let coordsText = coordsInput.value; // Split and trim whitespace from each value let splitUpVal = coordsText.split(",").map((v) => v.trim()); // Helper: convert string to boolean or null if invalid const toBoolean = (str) => { if (str.toLowerCase() === "true") return true; if (str.toLowerCase() === "false") return false; return null; }; if (splitUpVal.length !== 4 && splitUpVal.length !== 5) { console.log("You must input 4 or 5 arguments (comma-separated)."); return; } // Validate first 4 args as integers for (let i = 0; i < 4; i++) { if (!Number.isInteger(Number(splitUpVal[i]))) { console.log( "First 4 arguments must all be integers. Also make sure that there is no space in between numbers. Example of what not to do: ..., 34 6, ..." ); return; } } // organizing before sending data let tempCoords = { firstChunk: { x: Number(splitUpVal[0]), y: Number(splitUpVal[1]) }, secondChunk: { x: Number(splitUpVal[2]), y: Number(splitUpVal[3]) }, }; // With 5th arg (boolean) if (splitUpVal.length === 5) { let inputBool = toBoolean(splitUpVal[4]); if (inputBool === null) { console.log("The fifth argument only accepts 'true' or 'false'."); return; } multipleChunksDlUrl(tempCoords, inputBool); } else { // Only 4 args multipleChunksDlUrl(tempCoords); } }; let updateButtons = () => { let marker = document.querySelector(".maplibregl-marker"); if (!marker) { isPointing = false; chunkX = null; chunkY = null; pixelX = null; pixelY = null; chunkUrl = null; refreshSetPointsInfo(); } firstPointBtn.disabled = !isPointing; secPointBtn.disabled = !isPointing; let noChunkSelected = mlChunkCoords.firstChunk.x == null && mlChunkCoords.secondChunk.x == null; let hasAnUnassignedChunk = mlChunkCoords.firstChunk.x == null || mlChunkCoords.secondChunk.x == null; downloadChunkBtn.disabled = !isPointing || downloadingState; viewChunkBtn.disabled = !isPointing; downloadBtn.disabled = downloadingState || noChunkSelected; downloadRegionBtn.disabled = downloadingState || hasAnUnassignedChunk; manualDownloadBtn.disabled = downloadingState; highlightBtn.disabled = noChunkSelected; removePointsBtn.disabled = noChunkSelected; savePresetBtn.disabled = noChunkSelected; let presetDlBtns = document.querySelectorAll(".dlBtn"); presetDlBtns.forEach((btn) => { btn.disabled = downloadingState; }); }; // for the download bar let updateDownloadBar = () => { const progressElem = document.querySelector(".download-progress"); const textElem = document.querySelector(".download-text"); if (!progressElem || !textElem || totalImgsToBeDownloaded === 0) return; const percent = Math.min( 100, (currImgsDownloaded / totalImgsToBeDownloaded) * 100 ); progressElem.style.width = percent + "%"; textElem.textContent = `${currImgsDownloaded} / ${totalImgsToBeDownloaded}`; }; updateButtons(); const mlCoordsOrganizer = (mlCoords) => { console.log(mlCoords); let { firstChunk, secondChunk } = structuredClone(mlCoords); if (firstChunk.x == null && secondChunk.x == null) { console.error("Null on both coords"); return; } if (secondChunk.x == null) { secondChunk = { ...firstChunk }; return { firstChunk, secondChunk }; } else if (firstChunk.x == null) { firstChunk = { ...secondChunk }; return { firstChunk, secondChunk }; } // making sure that the coords that will be sent would be appropriate // turns the first point to be the topleft corner and the second the bottom right const result = { firstChunk: { x: Math.min(firstChunk.x, secondChunk.x), y: Math.min(firstChunk.y, secondChunk.y), }, secondChunk: { x: Math.max(firstChunk.x, secondChunk.x), y: Math.max(firstChunk.y, secondChunk.y), }, }; return result; }; let instanceInc = miscSection.querySelector(".instanceInc"); let instanceDec = miscSection.querySelector(".instanceDec"); instanceInc.addEventListener("click", () => changeInstances("inc")); instanceDec.addEventListener("click", () => changeInstances("dec")); let changeInstances = (type) => { let clamp = (num, min, max) => { return Math.min(Math.max(num, min), max); }; let instanceInfo = miscSection.querySelector(".instanceInfo"); if (type == "inc") { concurrentDlInstances++; } else if (type == "dec") { concurrentDlInstances--; } concurrentDlInstances = clamp(concurrentDlInstances, 1, maxDlInstances); savedConfigs.concurrentDlInstances = concurrentDlInstances; setGMValue("savedConfigs", savedConfigs); instanceInfo.textContent = `Download instances: ${concurrentDlInstances}`; }; // For other overlay scripts / to protect this script too lol const nativeFetch = window.fetch.bind(window); function makeFetchWrapper(fetchImpl) { const wrapper = async (resource, init) => { const url = new URL( typeof resource === "string" ? resource : resource.url || "", location.href // ensure absolute URL resolution ); const isTile = url.pathname.endsWith(".png"); const x = url.searchParams.get("x"); const y = url.searchParams.get("y"); // Always call the currently wrapped fetch implementation const res = await wrapper._target(resource, init); // Highlight Tile logic if ( isTile && isHightlightOn && highlightedChunksLinksArr.includes(url.href) && !downloadingState ) { const cloned = res.clone(); const blob = await cloned.blob(); const bmp = await createImageBitmap(blob); const canvas = document.createElement("canvas"); canvas.width = bmp.width; canvas.height = bmp.height; const ctx = canvas.getContext("2d"); ctx.drawImage(bmp, 0, 0); ctx.fillStyle = "rgba(0, 0, 255, 0.2)"; ctx.fillRect(0, 0, canvas.width, canvas.height); const modifiedBlob = await new Promise((resolve) => canvas.toBlob(resolve, "image/png") ); const headers = new Headers(res.headers); headers.delete("content-length"); headers.delete("content-encoding"); return new Response(modifiedBlob, { status: res.status, statusText: res.statusText, headers, }); } // Point Selection logic / settintg event listeners for "exit point" btns if (x != null && y != null) { const pathnames = url.pathname.split("/"); chunkX = Number(pathnames.at(-2)); chunkY = Number(pathnames.at(-1)); pixelX = Number(x); pixelY = Number(y); chunkUrl = `https://backend.wplace.live/files/s0/tiles/${chunkX}/${chunkY}.png`; isPointing = true; updateButtons(); refreshSetPointsInfo(); console.log(`Pressed on ChunkX: ${chunkX}, ChunkY: ${chunkY}`); const parent = document .querySelector(".rounded-t-box") ?.querySelector("div"); if (parent) { const pixelBtns = parent.querySelector(".hide-scrollbar"); let exitBtn = parent.querySelector( "div.px-3:nth-child(1) > button:nth-child(2)" ); let exitPointEvent = () => { isPointing = false; chunkX = null; chunkY = null; chunkUrl = null; updateButtons(); refreshSetPointsInfo(); }; if (exitBtn) { exitBtn.addEventListener("click", exitPointEvent); } let paintBtn = pixelBtns?.querySelector("button:nth-child(1)"); if (paintBtn) { paintBtn.addEventListener("click", exitPointEvent); } } else { console.error("Parent element not found"); } } // Default: return original response untouched return res; }; wrapper._target = fetchImpl; return wrapper; } // Installs the first wrapper around the native fetch let myFetchWrapper = makeFetchWrapper(nativeFetch); // Trap window.fetch so other scripts can patch safely << for Wplace Overlay Pro Object.defineProperty(window, "fetch", { configurable: true, get() { return myFetchWrapper; }, set(fn) { console.log("Another script patched fetch, wrapping it."); // Wrap the new fetch so recursion never happens myFetchWrapper = makeFetchWrapper(fn); }, }); // gonna need to optimize the code below, later. const multipleChunksDlUrl = async (chunkCoords, name = "", safety = true) => { if (downloadingState) return; let organizedChunkCoords = mlCoordsOrganizer(chunkCoords); let topleftX = organizedChunkCoords.firstChunk.x; let topleftY = organizedChunkCoords.firstChunk.y; let botRightX = organizedChunkCoords.secondChunk.x; let botRightY = organizedChunkCoords.secondChunk.y; console.log( `downloading chunks: ${organizedChunkCoords.firstChunk.x}, ${organizedChunkCoords.firstChunk.y} | ${organizedChunkCoords.secondChunk.x}, ${organizedChunkCoords.secondChunk.y}` ); let linksResultArr = getLinksFromChunkCoords(organizedChunkCoords); downloadingState = true; updateButtons(); let safetyThreshold = 70; let chunkWidth = 1 + Number(botRightX - topleftX); let imgsAmount = linksResultArr.length; if (linksResultArr.length > safetyThreshold) { if (safety) { console.warn( `You were about to download ${linksResultArr.length} images but was prevented by this precaution. If you intentionally wanted to download that much, you can type '${topLeftX}, ${topLeftY}, ${botRightX}, ${botRightY}, false' onto the manual chunk downloader. Good luck.` ); return; } else { console.log("Better pray to God..."); } } totalImgsToBeDownloaded = imgsAmount; currImgsDownloaded = 0; updateDownloadBar(); let resultCanvas = await stitchImages( linksResultArr, chunkWidth, concurrentDlInstances ); let canvasName; if (name == "") { canvasName = `ch-${topleftX}, ${topleftY}, ${botRightX}, ${botRightY} time-${Date.now()}`; } canvasName = `${name} - ${Date.now()}`; canvasDownloader(resultCanvas, canvasName); }; // region downloader const regionDl = async ( chunkCoords, pixelCoords, name = "", safety = true ) => { if ( pixelCoords.firstPixel.x == null || pixelCoords.secondPixel.x == null || downloadingState ) return; let organizedChunkCoords = mlCoordsOrganizer(chunkCoords); let topleftX = organizedChunkCoords.firstChunk.x; let topleftY = organizedChunkCoords.firstChunk.y; let botRightX = organizedChunkCoords.secondChunk.x; let botRightY = organizedChunkCoords.secondChunk.y; let linksResultArr = getLinksFromChunkCoords(organizedChunkCoords); downloadingState = true; updateButtons(); let safetyThreshold = 70; let chunkWidth = 1 + Number(botRightX - topleftX); let imgsAmount = linksResultArr.length; if (linksResultArr.length > safetyThreshold) { if (safety) { console.warn( `You were about to download ${linksResultArr.length} images but was prevented by this precaution. If you intentionally wanted to download that much, Turn off the 'safety' under the Region Download section. Good luck.` ); return; } else { console.log("Better pray to God..."); } } totalImgsToBeDownloaded = imgsAmount; currImgsDownloaded = 0; updateDownloadBar(); console.log( `First point coords: Chunkx: ${topleftX}, ChunkY: ${topleftY}, PixelX: ${pixelCoords.firstPixel.x}, PixelY: ${pixelCoords.firstPixel.y}` ); console.log( `Second point coords: Chunkx: ${botRightX}, ChunkY: ${botRightY}, PixelX: ${pixelCoords.secondPixel.x}, PixelY: ${pixelCoords.secondPixel.y}` ); let baseCanvas = await stitchImages( linksResultArr, chunkWidth, concurrentDlInstances ); console.log( `canvas - Width: ${baseCanvas.width}, Height: ${baseCanvas.height}` ); // translating the pixel coords onto canvas coords let toCanvasCoord = (chunk, pixel, origin, tileSize) => { return { x: (chunk.x - origin.x) * tileSize + pixel.x, y: (chunk.y - origin.y) * tileSize + pixel.y, }; }; // Basically the "organizedChunkCoords.firstChunk" serves as the "topleft corner" or the 0,0 coords // then we extract how far away are the points (on the amount of chunk tiles) from the topleft corner // then we multiply it by 1000 // then the result would be then added by the pixel's coordinate (on the current chunk it is on) to get its coords on canvas. let tileWidthAndHeight = 1000; // the width and height of a chunk, hopefully wont change lol let canvasPixelCoords = { firstPixel: toCanvasCoord( chunkCoords.firstChunk, pixelCoords.firstPixel, organizedChunkCoords.firstChunk, tileWidthAndHeight ), secondPixel: toCanvasCoord( chunkCoords.secondChunk, pixelCoords.secondPixel, organizedChunkCoords.firstChunk, tileWidthAndHeight ), }; console.log(chunkCoords); console.log(pixelCoords); console.log(canvasPixelCoords); let organizedCanvasPixelCoords = pixelCoordsOrganizer(canvasPixelCoords); // sets the width and height of the region let regionWidth = 1 + (organizedCanvasPixelCoords.secondPixel.x - organizedCanvasPixelCoords.firstPixel.x); let regionHeight = 1 + (organizedCanvasPixelCoords.secondPixel.y - organizedCanvasPixelCoords.firstPixel.y); // creates a new canvas so the data can be put into it let regionCanvas = document.createElement("canvas"); regionCanvas.width = regionWidth; regionCanvas.height = regionHeight; let regionCtx = regionCanvas.getContext("2d"); // copy the region onto the new canvas regionCtx.drawImage( baseCanvas, organizedCanvasPixelCoords.firstPixel.x, // source x organizedCanvasPixelCoords.firstPixel.y, // source y regionWidth, // source width regionHeight, // source height 0, // destination x 0, // destination y regionWidth, // destination width regionHeight // destination height ); let canvasName; if (name == "") { canvasName = `ch-${organizedChunkCoords.firstChunk.x}, ${ organizedChunkCoords.firstChunk.y } px-${organizedCanvasPixelCoords.firstPixel.x}, ${ organizedCanvasPixelCoords.firstPixel.y } size ${regionWidth}, ${regionHeight} time-${Date.now()}`; } else { canvasName = `${name} - ${Date.now()}`; } // finally download the resulting canvas await canvasDownloader(regionCanvas, canvasName); }; let pixelCoordsOrganizer = (pixelCoords) => { let { firstPixel, secondPixel } = structuredClone(pixelCoords); const result = { firstPixel: { x: Math.min(firstPixel.x, secondPixel.x), y: Math.min(firstPixel.y, secondPixel.y), }, secondPixel: { x: Math.max(firstPixel.x, secondPixel.x), y: Math.max(firstPixel.y, secondPixel.y), }, }; return result; }; let getLinksFromChunkCoords = (chunkCoords) => { console.log("getting the links from chunk coords."); console.log( "tempChunkCoords: " + `First chunk {x: ${chunkCoords.firstChunk.x}, y: ${chunkCoords.firstChunk.y}}, Second chunk {x: ${chunkCoords.secondChunk.x}, y: ${chunkCoords.secondChunk.y}}` ); let topleftX = chunkCoords.firstChunk.x, topleftY = chunkCoords.firstChunk.y, botRightX = chunkCoords.secondChunk.x, botRightY = chunkCoords.secondChunk.y; if (botRightX == null) { botRightX = topleftX; botRightY = topleftY; } let chunkWidth = 1 + Number(botRightX - topleftX); let chunkHeight = 1 + Number(botRightY - topleftY); console.log("chunkWidth: " + chunkWidth); console.log("chunkHeight: " + chunkHeight); let linksArr = []; for (let j = 0; j < chunkHeight; j++) { for (let i = 0; i < chunkWidth; i++) { linksArr.push( chunkTemplateUrl + (Number(i) + Number(topleftX)) + "/" + (Number(j) + Number(topleftY)) + ".png" ); } } return linksArr; }; async function stitchImages(images, width, concurrency, delay = 150) { const resizeImage = (img, maxWidth = 1000, maxHeight = 1000) => { if (img.width <= maxWidth && img.height <= maxHeight) { return img; // no need to resize } const scale = Math.min(maxWidth / img.width, maxHeight / img.height); const newWidth = Math.floor(img.width * scale); const newHeight = Math.floor(img.height * scale); const c = document.createElement("canvas"); c.width = newWidth; c.height = newHeight; const ctx = c.getContext("2d"); ctx.drawImage(img, 0, 0, newWidth, newHeight); const resized = new Image(); resized.src = c.toDataURL(); return new Promise((resolve) => { resized.onload = () => resolve(resized); }); }; const createBlank = () => { const c = document.createElement("canvas"); c.width = 1000; c.height = 1000; const ctx = c.getContext("2d"); ctx.fillStyle = "rgba(0,0,0,0)"; ctx.fillRect(0, 0, c.width, c.height); return new Promise((resolve) => { c.toBlob((blob) => { const img = new Image(); img.src = URL.createObjectURL(blob); img.onload = () => resolve(img); }); }); }; async function fetchAndLoad(src) { console.log("Downloading: " + src); while (true) { try { const res = await fetch(src, { mode: "cors" }); if (res.status === 429) { console.warn("Rate limited! Cooling down for 10s..."); await sleep(10000); continue; // retry } if (res.status === 404) { console.warn("404 Not Found, using blank:", src); return await createBlank(); } if (!res.ok) { throw new Error(`HTTP ${res.status}`); } const blob = await res.blob(); const img = await new Promise((resolve, reject) => { const image = new Image(); image.crossOrigin = "anonymous"; image.onload = () => { console.log("Done loading img"); resolve(image); }; image.onerror = () => reject(new Error("Decode error")); image.src = URL.createObjectURL(blob); }); return await resizeImage(img, 1000, 1000); } catch (err) { console.warn("Error loading, retrying:", src, err); await sleep(2000); // small cooldown before retry } } } // Worker pool async function loadImagesConcurrent(images, concurrency, delay) { const results = new Array(images.length); let index = 0; async function worker() { while (index < images.length) { const i = index++; const src = images[i]; const img = await fetchAndLoad(src); results[i] = img; currImgsDownloaded++; updateDownloadBar(); await sleep(delay); // per-worker cooldown } } const workers = Array.from({ length: concurrency }, () => worker()); // creates an array of promises (fetching imgs) await Promise.all(workers); return results; } // usage const loadedImages = await loadImagesConcurrent(images, concurrency, delay); // stitching const columns = width; const rows = Math.ceil(loadedImages.length / columns); const imgWidth = 1000; const imgHeight = 1000; const canvas = document.createElement("canvas"); canvas.width = imgWidth * columns; canvas.height = imgHeight * rows; const ctx = canvas.getContext("2d"); loadedImages.forEach((img, index) => { const x = (index % columns) * imgWidth; const y = Math.floor(index / columns) * imgHeight; ctx.drawImage(img, x, y); }); return canvas; } let canvasDownloader = async (canvasToBeDownloaded, name) => { canvasToBeDownloaded.toBlob((blob) => { const link = document.createElement("a"); link.href = URL.createObjectURL(blob); link.download = name + ".png"; document.body.appendChild(link); link.click(); document.body.removeChild(link); URL.revokeObjectURL(link.href); // to enable the highlight to stay after downloading downloadingState = false; updateButtons(); }, "image/png"); }; })();