Added image to canvas

This commit is contained in:
SwingTheVine 2025-07-30 02:47:03 -04:00
parent b7ec594907
commit 6face92136
8 changed files with 127 additions and 40 deletions

File diff suppressed because one or more lines are too long

View file

@ -34,8 +34,8 @@
<a href="https://github.com/SwingTheVine/Wplace-BlueMarble/releases" target="_blank"><img alt="Latest Release" src="https://img.shields.io/github/v/release/SwingTheVine/Wplace-BlueMarble?sort=date&style=flat&label=Latest%20Release&color=blue"></a>
<a href="https://github.com/SwingTheVine/Wplace-BlueMarble/blob/main/LICENSE.txt" target="_blank"><img alt="Software License: MPL-2.0" src="https://img.shields.io/badge/Software_License-MPL--2.0-brightgreen?style=flat"></a>
<a href="https://discord.gg/tpeBPy46hf" target="_blank"><img alt="Contact Me" src="https://img.shields.io/badge/Contact_Me-gray?style=flat&logo=Discord&logoColor=white&logoSize=auto&labelColor=cornflowerblue"></a>
<a href="" target="_blank"><img alt="WakaTime" src="https://img.shields.io/badge/Coding_Time-35hrs_30mins-blue?style=flat&logo=wakatime&logoColor=black&logoSize=auto&labelColor=white"></a>
<a href="" target="_blank"><img alt="Total Patches" src="https://img.shields.io/badge/Total_Patches-274-black?style=flat"></a>
<a href="" target="_blank"><img alt="WakaTime" src="https://img.shields.io/badge/Coding_Time-59hrs_0mins-blue?style=flat&logo=wakatime&logoColor=black&logoSize=auto&labelColor=white"></a>
<a href="" target="_blank"><img alt="Total Patches" src="https://img.shields.io/badge/Total_Patches-294-black?style=flat"></a>
<a href="" target="_blank"><img alt="Total Lines of Code" src="https://tokei.rs/b1/github/SwingTheVine/Wplace-BlueMarble?category=code"></a>
<a href="" target="_blank"><img alt="Total Comments" src="https://tokei.rs/b1/github/SwingTheVine/Wplace-BlueMarble?category=comments"></a>
<a href="" target="_blank"><img alt="Compression" src="https://img.shields.io/badge/Compression-70.19%25-blue"></a>

4
package-lock.json generated
View file

@ -1,12 +1,12 @@
{
"name": "wplace-bluemarble",
"version": "0.60.14",
"version": "0.63.20",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"name": "wplace-bluemarble",
"version": "0.60.14",
"version": "0.63.20",
"devDependencies": {
"esbuild": "^0.25.0",
"terser": "^5.43.1"

View file

@ -1,6 +1,6 @@
{
"name": "wplace-bluemarble",
"version": "0.63.0",
"version": "0.63.20",
"type": "module",
"scripts": {
"build": "node build/build.js",

View file

@ -1,7 +1,7 @@
// ==UserScript==
// @name Blue Marble
// @namespace https://github.com/SwingTheVine/
// @version 0.63.0
// @version 0.63.20
// @description A userscript to automate and/or enhance the user experience on Wplace.live. Make sure to comply with the site's Terms of Service, and rules! This script is not affiliated with Wplace.live in any way, use at your own risk. This script is not affiliated with TamperMonkey. The author of this userscript is not responsible for any damages, issues, loss of data, or punishment that may occur as a result of using this script. This script is provided "as is" under the MPL-2.0 license. The "Blue Marble" icon is licensed under CC0 1.0 Universal (CC0 1.0) Public Domain Dedication. The image is owned by NASA.
// @author SwingTheVine
// @license MPL-2.0

View file

@ -3,14 +3,17 @@
* @since 0.11.1
*/
import TemplateManager from "./templateManager.js";
import { escapeHTML, serverTPtoDisplayTP } from "./utils.js";
export default class ApiManager {
/** Constructor for ApiManager class
* @param {TemplateManager} templateManager
* @since 0.11.34
*/
constructor() {
constructor(templateManager) {
this.templateManager = templateManager;
this.disableAll = false; // Should the entire userscript be disabled?
this.coordsTilePixel = []; // Contains the last detected tile/pixel coordinate pair requested
}
@ -33,9 +36,13 @@ export default class ApiManager {
// Kills itself if the message was not intended for Blue Marble
if (!(data && data['source'] === 'blue-marble')) {return;}
// Kills itself if the message has no endpoint (intended for Blue Marble, but not this function)
if (!data['endpoint']) {return;}
// Trims endpoint to the second to last non-number, non-null directoy.
// E.g. "wplace.live/api/pixel/0/0?payload" -> "pixel"
const endpointText = data['endpoint'].split('?')[0].split('/').filter(s => s && isNaN(Number(s))).pop();
// E.g. "wplace.live/api/files/s0/tiles/0/0/0.png" -> "tiles"
const endpointText = data['endpoint']?.split('?')[0].split('/').filter(s => s && isNaN(Number(s))).filter(s => s && !s.includes('.')).pop();
console.log(`%cBlue Marble%c: Recieved message about "${endpointText}"`, 'color: cornflowerblue;', '');
@ -97,6 +104,21 @@ export default class ApiManager {
}
}
break;
case 'tiles':
const blobUUID = data['blobID'];
const blobData = data['blobData'];
const gojo = this.templateManager.drawGojo();
window.postMessage({
source: 'blue-marble',
blobID: blobUUID,
blobData: gojo,
blink: data['blink']
});
break;
case 'robots': // Request to retrieve what script types are allowed
this.disableAll = dataJSON['userscript']?.toString().toLowerCase() == 'false'; // Disables Blue Marble if site owner wants userscripts disabled

View file

@ -35,7 +35,25 @@ inject(() => {
const script = document.currentScript; // Gets the current script HTML Script Element
const name = script?.getAttribute('bm-name') || 'Blue Marble'; // Gets the name value that was passed in. Defaults to "Blue Marble" if nothing was found
const consoleStyle = script?.getAttribute('bm-cStyle') || ''; // Gets the console style value that was passed in. Defaults to no styling if nothing was found
const fetchedBlobQueue = new Map(); // Blobs being processed
window.addEventListener('message', (event) => {
const { source, endpoint, blobID, blobData, blink } = event.data;
const elapsed = Date.now() - blink;
// Since this code does not run in the userscript, we can't use consoleLog().
console.groupCollapsed(`%c${name}%c: ${fetchedBlobQueue.size} Recieved IMAGE message about blob "${blobID}"`, consoleStyle, '');
console.log(`Blob fetch took %c${String(Math.floor(elapsed/60000)).padStart(2,'0')}:${String(Math.floor(elapsed/1000) % 60).padStart(2,'0')}.${String(elapsed % 1000).padStart(3,'0')}%c MM:SS.mmm`, consoleStyle, '');
console.log(fetchedBlobQueue);
console.groupEnd();
// The modified blob won't have an endpoint, so we ignore any message without one.
if ((source == 'blue-marble') && !!blobID && !!blobData && !endpoint) {
fetchedBlobQueue.get(blobID)(blobData);
}
});
// Spys on "spontaneous" fetch requests made by the client
const originalFetch = window.fetch; // Saves a copy of the original fetch
@ -45,12 +63,13 @@ inject(() => {
const response = await originalFetch.apply(this, args); // Sends a fetch
const cloned = response.clone(); // Makes a copy of the response
// Retrieves the endpoint name. Unknown endpoint = "ignore"
const endpointName = ((args[0] instanceof Request) ? args[0]?.url : args[0]) || 'ignore';
// Check Content-Type to only process JSON
const contentType = cloned.headers.get('content-type') || '';
if (contentType.includes('application/json')) {
// Retrieves the endpoint name. Unknown endpoint = "ignore"
let endpointName = ((args[0] instanceof Request) ? args[0]?.url : args[0]) || 'ignore';
// Since this code does not run in the userscript, we can't use consoleLog().
console.log(`%c${name}%c: Sending JSON message about endpoint "${endpointName}"`, consoleStyle, '');
@ -67,30 +86,67 @@ inject(() => {
.catch(err => {
console.error(`%c${name}%c: Failed to parse JSON: `, consoleStyle, '', err);
});
} else if (contentType.includes('image/') && (!endpointName.includes('openfreemap'))) {
// Fetch custom for all images but opensourcemap
const blink = Date.now(); // Current time
const blob = await cloned.blob(); // The original blob
// Since this code does not run in the userscript, we can't use consoleLog().
console.log(`%c${name}%c: ${fetchedBlobQueue.size} Sending IMAGE message about endpoint "${endpointName}"`, consoleStyle, '');
// Returns the manipulated blob
return new Promise((resolve) => {
const blobUUID = crypto.randomUUID(); // Generates a random UUID
// Store the blob while we wait for processing
fetchedBlobQueue.set(blobUUID, (blobProcessed) => {
// The response that triggers when the blob is finished processing
// Creates a new response
resolve(new Response(blobProcessed, {
headers: cloned.headers,
status: cloned.status,
statusText: cloned.statusText
}));
// Removes the processed blob from the queue
fetchedBlobQueue.delete(blobUUID);
// Since this code does not run in the userscript, we can't use consoleLog().
console.log(`%c${name}%c: ${fetchedBlobQueue.size} Processed blob "${blobUUID}"`, consoleStyle, '');
});
window.postMessage({
source: 'blue-marble',
endpoint: endpointName,
blobID: blobUUID,
blobData: blob,
blink: blink
});
}).catch(exception => {
const elapsed = Date.now();
console.error(`%c${name}%c: Failed to Promise blob!`, consoleStyle, '');
console.groupCollapsed(`%c${name}%c: Details of failed blob Promise:`, consoleStyle, '');
console.log(`Endpoint: ${endpointName}\nThere are ${fetchedBlobQueue.size} blobs processing...\nBlink: ${blink.toLocaleString()}\nTime Since Blink: ${String(Math.floor(elapsed/60000)).padStart(2,'0')}:${String(Math.floor(elapsed/1000) % 60).padStart(2,'0')}.${String(elapsed % 1000).padStart(3,'0')} MM:SS.mmm`);
console.error(`Exception stack:`, exception);
console.groupEnd();
});
// cloned.blob().then(blob => {
// window.postMessage({
// source: 'blue-marble',
// endpoint: endpointName,
// blobData: blob
// }, '*');
// });
}
return response; // Returns the original response
};
});
/** What code to execute instantly in the client (webpage) to spy on the canvas.
* This code will execute outside of TamperMonkey's sandbox.
* @since 0.60.14
*/
inject(() => {
const script = document.currentScript; // Gets the current script HTML Script Element
const name = script?.getAttribute('bm-name') || 'Blue Marble'; // Gets the name value that was passed in. Defaults to "Blue Marble" if nothing was found
const consoleStyle = script?.getAttribute('bm-cStyle') || ''; // Gets the console style value that was passed in. Defaults to no styling if nothing was found
const originalMap = maplibregl.Map;
maplibregl.Map = function (...args) {
const instance = new originalMap(...args);
window.__bm_interceptedMap;
return instance;
}
});
// Imports the CSS file from dist folder on github
const cssOverlay = GM_getResourceText("CSS-BM-File");
GM_addStyle(cssOverlay);
@ -110,7 +166,7 @@ document.head.appendChild(stylesheetLink);
const observers = new Observers(); // Constructs a new Observers object
const overlay = new Overlay(name, version); // Constructs a new Overlay object
const templateManager = new TemplateManager(); // Constructs a new TemplateManager object
const apiManager = new ApiManager(); // Constructs a new ApiManager object
const apiManager = new ApiManager(templateManager); // Constructs a new ApiManager object
overlay.setApiManager(apiManager); // Sets the API manager

View file

@ -15,7 +15,7 @@ export default class TemplateManager {
this.onResize = this.onResize.bind(this); // Binds the handler for `resize` to this class instance's function
this.onZoom = this.onZoom.bind(this); // Binds the handler for `zoom` to this class instance's function
this.template = null; // The template image.
this.state = ''; // The state of the template ('blob', 'proccessing', 'template', etc.)
this.templateState = ''; // The state of the template ('blob', 'proccessing', 'template', etc.)
}
/** Retrieves the pixel art canvas.
@ -41,8 +41,10 @@ export default class TemplateManager {
canvasTemplateNew.style.position = 'absolute';
canvasTemplateNew.style.top = '0';
canvasTemplateNew.style.left = '0';
canvasTemplateNew.style.height = canvasMain?.style?.height;
canvasTemplateNew.style.width = canvasMain?.style?.width;
canvasTemplateNew.style.height = `${canvasMain?.clientHeight * (window.devicePixelRatio || 1)}px`;
canvasTemplateNew.style.width = `${canvasMain?.clientWidth * (window.devicePixelRatio || 1)}px`;
canvasTemplateNew.height = canvasMain?.clientHeight * (window.devicePixelRatio || 1);
canvasTemplateNew.width = canvasMain?.clientWidth * (window.devicePixelRatio || 1);
canvasTemplateNew.style.zIndex = '8999';
canvasTemplateNew.style.pointerEvents = 'none';
canvasMain?.parentElement?.appendChild(canvasTemplateNew); // Append the newCanvas as a child of the parent of the main canvas
@ -62,7 +64,7 @@ export default class TemplateManager {
setTemplateImage(file) {
this.template = file;
this.state = 'file';
this.templateState = 'file';
const url = URL.createObjectURL(file); // Creates a blob URL
window.open(url, '_blank'); // Opens a new tab with blob
@ -82,6 +84,13 @@ export default class TemplateManager {
}
}
drawGojo() {
if (this.templateState != 'file') {return;}
return this.template;
}
/** What to do to our canvas when the canvas is panned.
* @since 0.60.10
*/
@ -104,10 +113,10 @@ export default class TemplateManager {
const canvasMain = document.querySelector(this.canvasMainID);
const canvasTemplate = this.getCanvas();
if (!canvasTemplate || !canvasMain) {return;}
canvasTemplate.style.height = canvasMain.style.height;
canvasTemplate.style.width = canvasMain.style.width;
canvasTemplate.height = canvasMain.height;
canvasTemplate.width = canvasMain.width;
canvasTemplate.style.height = `${canvasMain?.clientHeight * (window.devicePixelRatio || 1)}px`;
canvasTemplate.style.width = `${canvasMain?.clientWidth * (window.devicePixelRatio || 1)}px`;
canvasTemplate.height = canvasMain?.clientHeight * (window.devicePixelRatio || 1);
canvasTemplate.width = canvasMain?.clientWidth * (window.devicePixelRatio || 1);
this.tempDraw();
}
}