Skip to content

Instantly share code, notes, and snippets.

@SoaringGecko
Forked from seleb/roll20 map save.js
Created November 14, 2021 00:55
Show Gist options
  • Save SoaringGecko/553fff2b9eeb968d89297eb17589884d to your computer and use it in GitHub Desktop.
Save SoaringGecko/553fff2b9eeb968d89297eb17589884d to your computer and use it in GitHub Desktop.
script for exporting roll20 maps to an image
/**
script for exporting roll20 maps to an image
how to use:
1. open your roll20 game to the page you want to save
2. open your browser's developer tools
3. copy-paste this entire file in the console
4. hit enter
5. wait for map to save and appear in top-left
6. right-click -> save as... to export
7. left-click to delete
notes:
- your UI will rapidly flash for a couple seconds when this is run,
as it is quickly scrolling through the map and saving it in chunks
- it's best to run this while at 100% zoom level.
it will automatically adjust if you aren't,
but sometimes the first chunk doesn't save properly anyway
- this script is unfortunately not 100% reliable,
as some assets may taint the canvas through CORS violations (i.e. technical reasons on roll20's end)
if this happens, you cannot save (even if you change pages) until they are removed and have refreshed.
of everything tested, icons attached to images on the map were the only things that caused this issue,
but i only tested a handful of free/web assets and no premium content
*/
// helper
// returns promise resolving on next animation frame
function raf() {
return new Promise(resolve => requestAnimationFrame(resolve));
}
// helper
function setZoom(zoom) {
try {
Array.from(document.querySelector('.selZoom').children).find(({
value
}) => value === zoom).click();
} catch (err) {
if (zoom !== 100) {
setZoom(100);
}
}
}
// main
async function saveMap() {
const curZoom = document.querySelector('.zoomClickBack').value;
const editorWrapper = document.querySelector('#editor-wrapper');
try {
console.log('saving map...');
// get total size
const scale = 70;
const page = window.Campaign.activePage();
const width = page.get('width') * scale;
const height = page.get('height') * scale;
// make a canvas to output to
const outputCanvas = document.createElement('canvas');
outputCanvas.width = width;
outputCanvas.height = height;
const ctx = outputCanvas.getContext('2d');
const finalCanvas = document.querySelector('#finalcanvas');
// set zoom to 100%
setZoom(100);
// give map a couple frames to update
await raf();
await raf();
// add some extra padding so we can scroll through fully
const editor = document.querySelector('#editor');
editor.style.paddingRight = `${finalCanvas.width}px`;
editor.style.paddingBottom = `${finalCanvas.height}px`;
// account for existing padding
const editorStyle = getComputedStyle(editor);
const paddingTop = parseInt(editorStyle.paddingTop, 10);
const paddingLeft = parseInt(editorStyle.paddingLeft, 10);
// scroll through and save chunks of map to output
const count = Math.ceil(width / finalCanvas.width) * Math.ceil(height / finalCanvas.height);
let progress = 0;
for (let oy = 0; oy < height; oy += finalCanvas.height) {
for (let ox = 0; ox < width; ox += finalCanvas.width) {
editorWrapper.scrollTop = oy + paddingTop;
editorWrapper.scrollLeft = ox + paddingLeft;
// need to wait for re-render
await raf();
ctx.drawImage(finalCanvas, ox + finalCanvas.parentElement.offsetLeft, oy + finalCanvas.parentElement.offsetTop);
console.log(`${Math.floor(++progress / count * 100)}%`);
}
}
// open output
var url = outputCanvas.toDataURL();
var img = document.createElement('img');
img.src = url;
img.style.position = 'fixed';
img.style.top = '1rem';
img.style.left = '8rem';
img.style.width = '10rem';
img.style.zIndex = '10000000';
img.style.cursor = 'pointer';
img.style.border = 'solid 1px red';
img.onclick = () => {
img.remove();
};
document.body.appendChild(img);
console.log('map saved!');
} finally {
// remove extra padding
editor.style.paddingRight = null;
editor.style.paddingBottom = null;
// reset zoom
setZoom(curZoom);
}
}
// actually run it
saveMap().catch(err => {
console.error(`something went wrong while saving map
if the error mentions an "insecure operation", your map may be tainted (see notes at top of script for more info)
`, err);
});
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment