-
-
Save SoaringGecko/553fff2b9eeb968d89297eb17589884d to your computer and use it in GitHub Desktop.
script for exporting roll20 maps to an image
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
/** | |
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