I'm using Mapbox GL, and trying to get a snapshot of it, and merge the snapshot with another image overlaid for output.
I have a HTMLCanvasElement
off screen, and I'm first writing the canvas returned from Map.getCanvas()
to it, then writing the second (alpha transparent) canvas over that.
The problem is that, though I clearly see elements onscreen in the Map instance, the result only shows the second image/canvas written, and the rest is blank.
So I export just the map's canvas, and I see it is because the map canvas is blank, although a console.log()
shows the image data from it to be a large chunk of information.
Here's my export function:
onExport(annotationCanvas: HTMLCanvasElement) {
const mergeCanvas: HTMLCanvasElement = document.createElement('canvas');
const mapCanvas: HTMLCanvasElement = this.host.map.getCanvas();
const mergeCtx: CanvasRenderingContext2D = mergeCanvas.getContext('2d');
mergeCanvas.height = annotationCanvas.height;
mergeCanvas.width = annotationCanvas.width;
mergeCtx.drawImage(mapCanvas, 0, 0);
mergeCtx.drawImage(annotationCanvas, 0, 0);
const mergedDataURL = mergeCanvas.toDataURL();
const mapDataURL = mapCanvas.toDataURL();
const annotationDataURL = annotationCanvas.toDataURL();
console.log(mapDataURL); // Lots of data
download(mapDataURL, 'map-data-only.png'); // Blank image @ 1920x1080
download(mergedDataURL, 'annotation.png'); // Only shows annotation (the second layer/canvas) data
}
Is this a bug, or am I missing something?
UPDATE: I sort of figured out what this is about, and have possible options.
Upon stumbling upon a Mapbox feature request, I learned that if you instantiate your Map
with the preserveDrawingBuffer
option set to false
(the default), you wont be able to get a canvas with usable image data. But setting this option to true
degrades performance. But you can't change this setting after a Map is instantiated...
I want the Map to perform the best it possibly can!!!!
So, on this answer I stumbled on, regarding a question about three.js, I learned that if I take the screenshot immediately after rendering, I will get the canvas/data that I need.
I tried just calling this.host.map['_rerender']()
right before I capture the canvas, but it still returned blankness.
Then searching around in the source code, I found a function called _requestRenderFrame
, that looks like it might be what I need, because I can ask the Map to run a function immediately after the next render cycle. But as I come to find out, for some reason, that function is omitted in the compiled code, whilst present in the source, apparently because it is only in the master, and not part of the release.
So I don't really have a satisfactory solution yet, so please let me know of any insights.
That's functional code. Note that Mapbox uses WebGl to render, so getContext('d2')
returns null
. It's better to create a canvas element, get the context, and set the map with the drawImage
function. I hope this helps.
let mapEl = document.querySelector('.mapboxgl-canvas');
let resizedCanvas = document.createElement("canvas");
let resizedContext = resizedCanvas.getContext("2d");
const isMapEmpty = mapEl.toDataURL() === "data:,";
if (isMapEmpty) {
console.log("The map is void");
} else {
console.log("The map has information");
}
resizedCanvas.height = "1035";
resizedCanvas.width = "755";
resizedContext.drawImage(mapEl, 0, 0, 755, 1035);
let b64 = await mergeImages([
{
src: resizedCanvas.toDataURL(),
x: 297,
y: 16
},
{
src: "/assets/anotherImage.png"
}
]
)