Skip to content

Commit d57a6bf

Browse files
committed
Documentation and cleanup pass.
- Make styles internally consistent - Add some documentation and JSDoc for methods - Add some attribution for the rendering of the `overlayPane`
1 parent 9e8ac17 commit d57a6bf

3 files changed

Lines changed: 54 additions & 8 deletions

File tree

src/GameMap.js

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -233,7 +233,7 @@ export default class GameMap
233233
bounds : this.getBounds(),
234234
maxZoom : (this.maxTileZoom + 4),
235235
maxNativeZoom : this.maxTileZoom,
236-
crossOrigin : true,
236+
crossOrigin : true, // We need this in order to be able to render tiles to a canvas. Otherwise, we'd need to re-fetch all the tiles.
237237
};
238238

239239
this.baseLayer = 'gameLayer';
@@ -244,6 +244,7 @@ export default class GameMap
244244
this.leafletMap.setMaxBounds(this.getBounds());
245245
this.leafletMap.fitBounds(this.getBounds());
246246

247+
// Add a button to export the current viewport as an image and then download it.
247248
this.exportControl = L.control.exportControl({});
248249
this.leafletMap.addControl(this.exportControl);
249250

src/Lib/LeafletPlugins.js

Lines changed: 46 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -113,6 +113,11 @@ L.control.sliderControl = function(options)
113113
};
114114

115115
L.TileLayer.include({
116+
/**
117+
* Render the tile layer to a canvas with the same dimensions as the map's viewport.
118+
* @param {Function} callback A callback called when rendering to the canvas is complete. Standard callback of `Function(err?, canvas?)` signature
119+
* @returns {void}
120+
*/
116121
renderToCanvas: function(callback) {
117122
// Defer until we're done loading tiles
118123
if (this._loading) {
@@ -130,6 +135,16 @@ L.TileLayer.include({
130135
return callback(new Error('Unable to render to canvas: Zoom out of range!'));;
131136
}
132137

138+
// There's a lot of information we need from the map to understand where a tile should be rendered
139+
// This approach cheats a little bit: instead of trying to figure out which tiles need to be rendered,
140+
// we assume they all already are loaded and rendered correctly. This lets us sidestep a handful of steps
141+
// So, starting from the list of loaded and active tiles, we need to create a canvas with the same dimensions
142+
// as the map viewport, and then render into each each tile.
143+
// This is complicated by the way `leaflet` works: not only do we have to understand where a tile exists
144+
// with respect to the layer, we also have to understand where that layer exists with respect to the map
145+
// and whether or not that layer has had any zoom transformations applied.
146+
// Once we have all that information, we can apply the necessary transformations to the tile to place
147+
// it in the correct location.
133148
const zoom = this._clampZoom(map.getZoom());
134149
const layerScale = map.getZoomScale(zoom, this._tileZoom);
135150
const level = this._levels[this._tileZoom];
@@ -140,7 +155,7 @@ L.TileLayer.include({
140155
const scaledSize = tileSize.multiplyBy(layerScale);
141156
const canvasDimensions = map.getSize();
142157

143-
// This canvas is the size of all the tiles that are currently visible, partial or otherwise:
158+
// This canvas should be the same size as the map's viewport:
144159
const canvas = document.createElement('canvas');
145160
canvas.width = canvasDimensions.x;
146161
canvas.height = canvasDimensions.y;
@@ -153,6 +168,11 @@ L.TileLayer.include({
153168
console.warn(`Missing tile ${tile.coords}`, tile);
154169
continue;
155170
} else {
171+
// We can get the tile's position with respect to the layer with `_getTilePos`, but
172+
// we also need to scale/translate it according to the tile container's transformations,
173+
// and then the map pane's translation
174+
// We take advantage of the 2d context's ability to do the scaling for us.
175+
// Note: this may not be precise: see the hack at the end of `GameMap.js`
156176
const offset = this._getTilePos(tile.coords).multiplyBy(layerScale).add(layerTranslate).add(mapTranslate);
157177
context.drawImage(tile.el, 0, 0, tileSize.x, tileSize.y, offset.x, offset.y, scaledSize.x, scaledSize.y);
158178
}
@@ -162,6 +182,14 @@ L.TileLayer.include({
162182
}
163183
});
164184

185+
/**
186+
* Iterate over an array in an asynchronous way. The item callback will be provided with a `next` callback, to be called when
187+
* time to move on to the next item. Call `next` with an error to abort iteration early. `done` will receive this error.
188+
* @param {Array<any>} arr The array to iterate over
189+
* @param {Function} callback The callback to execute on each item. Called with two arguments: the current item, and `next`, to be called when done
190+
* @param {Function} done A callback to be called at the end of iteration. Receives two arg
191+
* @returns {void}
192+
*/
165193
const eachAsync = (arr, callback, done) => {
166194
let i = 0;
167195
const len = arr.length;
@@ -173,6 +201,7 @@ const eachAsync = (arr, callback, done) => {
173201
if (i < len) {
174202
callback(arr[i], () => {
175203
i++;
204+
// We use requestAnimationFrame to break out of the call stack. Probably overkill here.
176205
requestAnimationFrame(() => next());
177206
});
178207
} else {
@@ -184,6 +213,10 @@ const eachAsync = (arr, callback, done) => {
184213
}
185214

186215
L.Map.include({
216+
/**
217+
* If we have an overlay pane, render it to a canvas
218+
* @returns {null|HTMLCanvasElement}
219+
*/
187220
_renderOverlay: function() {
188221
if (!this._panes) {
189222
return null;
@@ -194,6 +227,7 @@ L.Map.include({
194227
return null;
195228
}
196229

230+
// This is taken from the `leaflet-image` plugin, with some small modifications
197231
const dimensions = this.getSize();
198232
const bounds = this.getPixelBounds(),
199233
origin = this.getPixelOrigin(),
@@ -211,10 +245,17 @@ L.Map.include({
211245
return null;
212246
},
213247

248+
/**
249+
* Render the entire map to a canvas. This canvas can then be saved as an image with something like
250+
* FileSaver.js
251+
* @param {Function} callback Callback that receives (err?, canvas?) when done generating the image
252+
*/
214253
renderToCanvas: function(callback) {
215254
const layers = [];
255+
// Right now, we only support rendering `TileLayer` layers and the overlay pane.
216256
const tileLayers = Object.values(this._layers).filter(l => l instanceof L.TileLayer);
217257
eachAsync(tileLayers, (layer, next) => {
258+
// We'll render each layer to a separate canvas, then composite them at the end.
218259
layer.renderToCanvas((err, canvas) => {
219260
if (err) {
220261
return next(err);
@@ -227,6 +268,7 @@ L.Map.include({
227268
return callback(err);
228269
}
229270

271+
// If we have an overlay, let's render it out to a canvas, as well.
230272
const overlay = this._renderOverlay();
231273
// if we have an overlay layer, draw it last
232274
if (overlay) {
@@ -254,13 +296,11 @@ L.Control.ExportControl = L.Control.extend({
254296
position: 'topleft',
255297
},
256298

257-
initialize: function(options)
258-
{
299+
initialize: function(options) {
259300
L.Util.setOptions(this, options);
260301
},
261302

262-
onAdd: function(map)
263-
{
303+
onAdd: function(map) {
264304
this.options.map = map;
265305
const className = 'leaflet-control-zoom leaflet-bar';
266306
const container = L.DomUtil.create('div', className);
@@ -284,6 +324,7 @@ L.Control.ExportControl = L.Control.extend({
284324
onRemove: function(){},
285325

286326
_exportImage: function() {
327+
// This seems janky, since this control is added via `GameMap`...
287328
window.SCIM.exportImage();
288329
}
289330
});

src/SCIM.js

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,3 @@
1-
/* global L */
21
import BaseLayout from './BaseLayout.js';
32
import GameMap from './GameMap.js';
43
import SaveParser from './SaveParser.js';
@@ -229,7 +228,12 @@ export default class SCIM
229228
return true;
230229
};
231230

232-
// Export map as image
231+
/**
232+
* Export the current viewport as a png. If there is a save game loaded, this png will have the same name as the save game file.
233+
* If not, it will be named `export.png`
234+
* @async
235+
* @returns {void}
236+
*/
233237
exportImage() {
234238
// no map?
235239
if (!this.map) {

0 commit comments

Comments
 (0)