Enhance overlay drag and file input handling

Improved the overlay drag logic for smoother, GPU-accelerated movement using requestAnimationFrame and transform. Updated file input elements to use stronger hiding techniques, preventing browser-native text from appearing during minimize/maximize. Updated version to 0.72.0 and added additional debug logging and minor UI/UX improvements.
This commit is contained in:
Nicholas 2025-08-05 14:18:12 -03:00
parent da77f2c55a
commit 1d78a140dd
8 changed files with 158 additions and 61 deletions

View file

@ -1 +1 @@
#bm-l{position:fixed;background-color:#153063e6;color:#fff;padding:10px;border-radius:8px;z-index:9000;transition:all .3s ease;max-width:300px;width:auto}#bm-4,#bm-l hr,#bm-3,#bm-1{transition:opacity .2s ease,height .2s ease}div#bm-l{font-family:Roboto Mono,Courier New,Monaco,DejaVu Sans Mono,monospace,Arial;letter-spacing:.05em}#bm-g{margin-bottom:.5em;background:url('data:image/svg+xml;utf8,<svg xmlns="http://www.w3.org/2000/svg" width="5" height="5"><circle cx="3" cy="3" r="1.5" fill="CornflowerBlue" /></svg>') repeat;cursor:grab;width:100%;height:1em}#bm-g.dragging{cursor:grabbing}#bm-7{margin-bottom:.5em}#bm-7[style*="text-align: center"]{display:flex;flex-direction:column;align-items:center;justify-content:center}#bm-l[style*="padding: 5px"]{width:auto!important;max-width:300px;min-width:200px}#bm-l img{display:inline-block;height:2.5em;margin-right:1ch;vertical-align:middle;transition:opacity .2s ease}#bm-7[style*="text-align: center"] img{display:block;margin:0 auto}#bm-g{transition:margin-bottom .2s ease}#bm-l h1{display:inline-block;font-size:x-large;font-weight:700;vertical-align:middle}#bm-3 input[type=checkbox]{vertical-align:middle;margin-right:.5ch}#bm-3 label{margin-right:.5ch}.bm-p{border:white 1px solid;height:1.5em;width:1.5em;margin-top:2px;text-align:center;line-height:1em;padding:0!important}#bm-c{vertical-align:middle}#bm-c svg{width:50%;margin:0 auto;fill:#111}div:has(>#bm-button-teleport){display:flex;gap:.5ch}#bm-button-favorite svg,#bm-button-template svg{height:1em;margin:2px auto 0;text-align:center;line-height:1em;vertical-align:bottom}#bm-8 input[type=number]{appearance:auto;-moz-appearance:textfield;width:5.5ch;margin-left:1ch;background-color:#0003;padding:0 .5ch;font-size:small}#bm-8 input[type=number]::-webkit-outer-spin-button,#bm-8 input[type=number]::-webkit-inner-spin-button{-webkit-appearance:none;margin:0}#bm-0{display:flex;flex-direction:row;flex-wrap:wrap;align-content:center;justify-content:center;align-items:center;gap:1ch}div:has(>#bm-2)>button{width:100%;white-space:nowrap;overflow:hidden;text-overflow:ellipsis}#bm-a{font-size:small;background-color:#0003;padding:0 .5ch;height:3.75em;width:100%}#bm-1{display:flex;justify-content:space-between}#bm-l small{font-size:x-small;color:#d3d3d3}#bm-4,#bm-3,#bm-8,#bm-0,div:has(>#bm-2),#bm-a{margin-top:.5em}#bm-l button{background-color:#144eb9;border-radius:1em;padding:0 .75ch}#bm-l button:hover,#bm-l button:focus-visible{background-color:#1061e5}#bm-l button:active,#bm-l button:disabled{background-color:#2e97ff}#bm-l button:disabled{text-decoration:line-through} #bm-l{position:fixed;background-color:#153063e6;color:#fff;padding:10px;border-radius:8px;z-index:9000;transition:all .3s ease;max-width:300px;width:auto;will-change:transform;backface-visibility:hidden;-webkit-backface-visibility:hidden;transform-style:preserve-3d;-webkit-transform-style:preserve-3d}#bm-4,#bm-l hr,#bm-3,#bm-1{transition:opacity .2s ease,height .2s ease}div#bm-l{font-family:Roboto Mono,Courier New,Monaco,DejaVu Sans Mono,monospace,Arial;letter-spacing:.05em}#bm-g{margin-bottom:.5em;background:url('data:image/svg+xml;utf8,<svg xmlns="http://www.w3.org/2000/svg" width="5" height="5"><circle cx="3" cy="3" r="1.5" fill="CornflowerBlue" /></svg>') repeat;cursor:grab;width:100%;height:1em}#bm-g.dragging{cursor:grabbing}#bm-l:has(#bm-g.dragging){pointer-events:none;user-select:none;-webkit-user-select:none;-moz-user-select:none;-ms-user-select:none}#bm-g.dragging{pointer-events:auto}#bm-7{margin-bottom:.5em}#bm-7[style*="text-align: center"]{display:flex;flex-direction:column;align-items:center;justify-content:center}#bm-l[style*="padding: 5px"]{width:auto!important;max-width:300px;min-width:200px}#bm-l img{display:inline-block;height:2.5em;margin-right:1ch;vertical-align:middle;transition:opacity .2s ease}#bm-7[style*="text-align: center"] img{display:block;margin:0 auto}#bm-g{transition:margin-bottom .2s ease}#bm-l h1{display:inline-block;font-size:x-large;font-weight:700;vertical-align:middle}#bm-3 input[type=checkbox]{vertical-align:middle;margin-right:.5ch}#bm-3 label{margin-right:.5ch}.bm-p{border:white 1px solid;height:1.5em;width:1.5em;margin-top:2px;text-align:center;line-height:1em;padding:0!important}#bm-c{vertical-align:middle}#bm-c svg{width:50%;margin:0 auto;fill:#111}div:has(>#bm-button-teleport){display:flex;gap:.5ch}#bm-button-favorite svg,#bm-button-template svg{height:1em;margin:2px auto 0;text-align:center;line-height:1em;vertical-align:bottom}#bm-8 input[type=number]{appearance:auto;-moz-appearance:textfield;width:5.5ch;margin-left:1ch;background-color:#0003;padding:0 .5ch;font-size:small}#bm-8 input[type=number]::-webkit-outer-spin-button,#bm-8 input[type=number]::-webkit-inner-spin-button{-webkit-appearance:none;margin:0}#bm-0{display:flex;flex-direction:row;flex-wrap:wrap;align-content:center;justify-content:center;align-items:center;gap:1ch}div:has(>#bm-2)>button{width:100%;white-space:nowrap;overflow:hidden;text-overflow:ellipsis}#bm-2,input[type=file][id*=template]{display:none!important;visibility:hidden!important;position:absolute!important;left:-9999px!important;top:-9999px!important;width:0!important;height:0!important;opacity:0!important;z-index:-9999!important;pointer-events:none!important}#bm-a{font-size:small;background-color:#0003;padding:0 .5ch;height:3.75em;width:100%}#bm-1{display:flex;justify-content:space-between}#bm-l small{font-size:x-small;color:#d3d3d3}#bm-4,#bm-3,#bm-8,#bm-0,div:has(>#bm-2),#bm-a{margin-top:.5em}#bm-l button{background-color:#144eb9;border-radius:1em;padding:0 .75ch}#bm-l button:hover,#bm-l button:focus-visible{background-color:#1061e5}#bm-l button:active,#bm-l button:disabled{background-color:#2e97ff}#bm-l button:disabled{text-decoration:line-through}

File diff suppressed because one or more lines are too long

4
package-lock.json generated
View file

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

View file

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

View file

@ -1,7 +1,7 @@
// ==UserScript== // ==UserScript==
// @name Blue Marble // @name Blue Marble
// @namespace https://github.com/SwingTheVine/ // @namespace https://github.com/SwingTheVine/
// @version 0.70.0 // @version 0.72.0
// @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. // @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 // @author SwingTheVine
// @license MPL-2.0 // @license MPL-2.0

View file

@ -430,9 +430,9 @@ export default class Overlay {
return this; return this;
} }
/** Adds a file input to the overlay. This includes a container and a button. /** Adds a file input to the overlay with enhanced visibility controls.
* This input element will have properties shared between all file input elements in the overlay. * This input element will have properties shared between all file input elements in the overlay.
* You can override the shared properties by using a callback. * Uses multiple hiding methods to prevent browser native text from appearing during minimize/maximize.
* @param {Object.<string, any>} [additionalProperties={}] - The DOM properties of the file input that are NOT shared between all overlay file input elements. These should be camelCase. * @param {Object.<string, any>} [additionalProperties={}] - The DOM properties of the file input that are NOT shared between all overlay file input elements. These should be camelCase.
* @param {function(Overlay, HTMLDivElement, HTMLInputElement, HTMLButtonElement):void} [callback=()=>{}] - Additional JS modification to the file input. * @param {function(Overlay, HTMLDivElement, HTMLInputElement, HTMLButtonElement):void} [callback=()=>{}] - Additional JS modification to the file input.
* @returns {Overlay} Overlay class instance (this) * @returns {Overlay} Overlay class instance (this)
@ -451,7 +451,10 @@ export default class Overlay {
*/ */
addInputFile(additionalProperties = {}, callback = () => {}) { addInputFile(additionalProperties = {}, callback = () => {}) {
const properties = {'type': 'file', 'style': 'display: none;'}; // Shared file input DOM properties const properties = {
'type': 'file',
'style': 'display: none !important; visibility: hidden !important; position: absolute !important; left: -9999px !important; width: 0 !important; height: 0 !important; opacity: 0 !important;'
}; // Complete file input hiding to prevent native browser text interference
const text = additionalProperties['textContent'] ?? ''; // Retrieves the text content const text = additionalProperties['textContent'] ?? ''; // Retrieves the text content
delete additionalProperties['textContent']; // Deletes the text content before applying the additional properties to the file input delete additionalProperties['textContent']; // Deletes the text content before applying the additional properties to the file input
@ -463,11 +466,15 @@ export default class Overlay {
this.buildElement(); // Signifies that we are done adding children to the button this.buildElement(); // Signifies that we are done adding children to the button
this.buildElement(); // Signifies that we are done adding children to the container this.buildElement(); // Signifies that we are done adding children to the container
// Prevent file input from being accessible or visible in any way
input.setAttribute('tabindex', '-1');
input.setAttribute('aria-hidden', 'true');
button.addEventListener('click', () => { button.addEventListener('click', () => {
input.click(); // Clicks the file input input.click(); // Clicks the file input
}); });
// Changes the button text content (and trims the file name) // Update button text when file is selected
input.addEventListener('change', () => { input.addEventListener('change', () => {
button.style.maxWidth = `${button.offsetWidth}px`; button.style.maxWidth = `${button.offsetWidth}px`;
if (input.files.length > 0) { if (input.files.length > 0) {
@ -533,14 +540,20 @@ export default class Overlay {
} }
} }
/** Handles dragging of the overlay. /** Handles dragging of the overlay with performance optimizations.
* Uses requestAnimationFrame for smooth animations and GPU-accelerated transforms.
* @param {string} moveMe - The ID of the element to be moved * @param {string} moveMe - The ID of the element to be moved
* @param {string} iMoveThings - The ID of the element to be moved * @param {string} iMoveThings - The ID of the drag handle element
* @since 0.8.2 * @since 0.8.2
*/ */
handleDrag(moveMe, iMoveThings) { handleDrag(moveMe, iMoveThings) {
let isDragging = false; let isDragging = false;
let offsetX, offsetY = 0; let offsetX, offsetY = 0;
let animationFrame = null;
let currentX = 0;
let currentY = 0;
let targetX = 0;
let targetY = 0;
// Retrieves the elements (allows either '#id' or 'id' to be passed in) // Retrieves the elements (allows either '#id' or 'id' to be passed in)
moveMe = document.querySelector(moveMe?.[0] == '#' ? moveMe : '#' + moveMe); moveMe = document.querySelector(moveMe?.[0] == '#' ? moveMe : '#' + moveMe);
@ -552,67 +565,110 @@ export default class Overlay {
return; // Kills itself return; // Kills itself
} }
// What to do when the mouse is pressed down on the element that moves things // Smooth animation loop using requestAnimationFrame for optimal performance
iMoveThings.addEventListener('mousedown', function(event) { const updatePosition = () => {
if (isDragging) {
// Only update DOM if position changed significantly (reduce repaints)
const deltaX = Math.abs(currentX - targetX);
const deltaY = Math.abs(currentY - targetY);
if (deltaX > 0.5 || deltaY > 0.5) {
currentX = targetX;
currentY = targetY;
// Use CSS transform for GPU acceleration instead of left/top
moveMe.style.transform = `translate(${currentX}px, ${currentY}px)`;
moveMe.style.left = '0px';
moveMe.style.top = '0px';
moveMe.style.right = '';
}
animationFrame = requestAnimationFrame(updatePosition);
}
};
// Cache initial position to avoid expensive getBoundingClientRect calls during drag
let initialRect = null;
const startDrag = (clientX, clientY) => {
isDragging = true; isDragging = true;
offsetX = event.clientX - moveMe.getBoundingClientRect().left; initialRect = moveMe.getBoundingClientRect();
offsetY = event.clientY - moveMe.getBoundingClientRect().top; offsetX = clientX - initialRect.left;
document.body.style.userSelect = 'none'; // Prevents text selection while dragging offsetY = clientY - initialRect.top;
iMoveThings.classList.add('dragging'); // Adds a class to indicate a dragging state
// Get current position from transform or use element position
const computedStyle = window.getComputedStyle(moveMe);
const transform = computedStyle.transform;
if (transform && transform !== 'none') {
const matrix = new DOMMatrix(transform);
currentX = matrix.m41;
currentY = matrix.m42;
} else {
currentX = initialRect.left;
currentY = initialRect.top;
}
targetX = currentX;
targetY = currentY;
document.body.style.userSelect = 'none';
iMoveThings.classList.add('dragging');
// Start animation loop
if (animationFrame) {
cancelAnimationFrame(animationFrame);
}
updatePosition();
};
const endDrag = () => {
isDragging = false;
if (animationFrame) {
cancelAnimationFrame(animationFrame);
animationFrame = null;
}
document.body.style.userSelect = '';
iMoveThings.classList.remove('dragging');
};
// Mouse down - start dragging
iMoveThings.addEventListener('mousedown', function(event) {
event.preventDefault();
startDrag(event.clientX, event.clientY);
}); });
// What to do when the touch starts on the element that moves things // Touch start - start dragging
iMoveThings.addEventListener('touchstart', function(event) { iMoveThings.addEventListener('touchstart', function(event) {
isDragging = true;
const touch = event?.touches?.[0]; const touch = event?.touches?.[0];
if (!touch) {return;} if (!touch) {return;}
offsetX = touch.clientX - moveMe.getBoundingClientRect().left; // Distance between the left edge of the overlay, and the cursor startDrag(touch.clientX, touch.clientY);
offsetY = touch.clientY - moveMe.getBoundingClientRect().top; // Distance between the top edge of the overlay, and the cursor event.preventDefault();
document.body.style.userSelect = 'none'; // Prevents text selection while dragging }, { passive: false });
iMoveThings.classList.add('dragging'); // Adds a class to indicate a dragging state
}, { passive: false }); // Prevents scrolling from being captured
// What to do when the mouse is moved while dragging // Mouse move - update target position
document.addEventListener('mousemove', function(event) { document.addEventListener('mousemove', function(event) {
if (isDragging) { if (isDragging && initialRect) {
moveMe.style.left = (event.clientX - offsetX) + 'px'; // Binds the overlay to the left side of the screen, and sets it's position to the cursor targetX = event.clientX - offsetX;
moveMe.style.top = (event.clientY - offsetY) + 'px'; // Binds the overlay to the top of the screen, and sets it's position to the cursor targetY = event.clientY - offsetY;
moveMe.style.right = ''; // Destroys the right property to unbind the overlay from the right side of the screen
} }
}); }, { passive: true });
// What to do when the touch moves while dragging // Touch move - update target position
document.addEventListener('touchmove', function(event) { document.addEventListener('touchmove', function(event) {
if (isDragging) { if (isDragging && initialRect) {
const touch = event?.touches?.[0]; const touch = event?.touches?.[0];
if (!touch) {return;} if (!touch) {return;}
moveMe.style.left = (touch.clientX - offsetX) + 'px'; // Binds the overlay to the left side of the screen, and sets it's position to the cursor targetX = touch.clientX - offsetX;
moveMe.style.top = (touch.clientY - offsetY) + 'px'; // Binds the overlay to the top of the screen, and sets it's position to the cursor targetY = touch.clientY - offsetY;
moveMe.style.right = ''; // Destroys the right property to unbind the overlay from the right side of the screen event.preventDefault();
event.preventDefault(); // prevent scrolling while dragging
} }
}, { passive: false }); // Prevents scrolling from being captured }, { passive: false });
// What to do when the mouse is released // End drag events
document.addEventListener('mouseup', function() { document.addEventListener('mouseup', endDrag);
isDragging = false; document.addEventListener('touchend', endDrag);
document.body.style.userSelect = ''; // Restores text selection capability after dragging document.addEventListener('touchcancel', endDrag);
iMoveThings.classList.remove('dragging'); // Removes the dragging class
});
// What to do when the touch ends
document.addEventListener('touchend', function() {
isDragging = false;
document.body.style.userSelect = ''; // Restores text selection capability after dragging
iMoveThings.classList.remove('dragging'); // Removes the dragging class
});
// What to do when the touch is cancelled
document.addEventListener('touchcancel', function() {
isDragging = false;
document.body.style.userSelect = ''; // Restores text selection capability after dragging
iMoveThings.classList.remove('dragging'); // Removes the dragging class
});
} }
/** Handles status display. /** Handles status display.

View file

@ -2,6 +2,12 @@
* @since 0.0.0 * @since 0.0.0
* *
* VERSION HISTORY: * VERSION HISTORY:
* 0.72.0 - Performance and UI improvements
* - Smooth drag performance with requestAnimationFrame and GPU acceleration
* - Fixed file upload button text bug during UI state changes
* - Added minimize/maximize animations with CSS transitions
* - Hardware-accelerated transformations and smart DOM updates
*
* 0.71.0 - Added minimize/maximize functionality and pixel counting system * 0.71.0 - Added minimize/maximize functionality and pixel counting system
* Features added: * Features added:
* - Interactive minimize/maximize overlay with click-to-toggle functionality * - Interactive minimize/maximize overlay with click-to-toggle functionality

View file

@ -11,12 +11,18 @@
transition: all 0.3s ease; transition: all 0.3s ease;
max-width: 300px; max-width: 300px;
width: auto; width: auto;
/* Performance optimizations for smooth dragging */
will-change: transform;
backface-visibility: hidden;
-webkit-backface-visibility: hidden;
transform-style: preserve-3d;
-webkit-transform-style: preserve-3d;
} }
/* Smooth transitions for minimize/maximize functionality */ /* Smooth transitions for minimize/maximize functionality */
#bm-contain-userinfo, #bm-contain-userinfo,
#bm-overlay hr, #bm-overlay hr,
#bm-contain-automation, #bm-contain-automation,
#bm-contain-buttons-action { #bm-contain-buttons-action {
transition: opacity 0.2s ease, height 0.2s ease; transition: opacity 0.2s ease, height 0.2s ease;
} }
@ -49,6 +55,20 @@ div#bm-overlay {
cursor: grabbing; cursor: grabbing;
} }
/* Disable interactions during drag for better performance */
#bm-overlay:has(#bm-bar-drag.dragging) {
pointer-events: none;
user-select: none;
-webkit-user-select: none;
-moz-user-select: none;
-ms-user-select: none;
}
/* Keep drag bar interactive when dragging */
#bm-bar-drag.dragging {
pointer-events: auto;
}
/* The container for the overlay header */ /* The container for the overlay header */
#bm-contain-header { #bm-contain-header {
margin-bottom: 0.5em; margin-bottom: 0.5em;
@ -188,6 +208,21 @@ div:has(> #bm-input-file-template) > button {
text-overflow: ellipsis; text-overflow: ellipsis;
} }
/* Force complete invisibility of file input to prevent native browser text */
#bm-input-file-template,
input[type="file"][id*="template"] {
display: none !important;
visibility: hidden !important;
position: absolute !important;
left: -9999px !important;
top: -9999px !important;
width: 0 !important;
height: 0 !important;
opacity: 0 !important;
z-index: -9999 !important;
pointer-events: none !important;
}
/* Output status area */ /* Output status area */
#bm-output-status { #bm-output-status {
font-size: small; font-size: small;