Cannot pitch canvas layer correctly in Mapbox GL JS

Viewed 351

I am moving some code from Leaflet into Mapbox GL JS. I have a 2d animated canvas which is rendered correctly when viewed top-down (pitch 0), rotated (bearing whatever), but is skewed when the pitch isn't zero. I have spent the past few days trying to correct it without much success.

My code needs to be able to convert between screen dimensions and map coordinates. E.g. for a given pixel, what lat-lon results (with map.project([lon, lat]) ). Or for a lat-lon, what pixel does it correspond to (with map.unproject([x,y]) ). When the canvas is rendered with pitch 0, the latitudes are equally spaced (*enough) across the pixels and everything works as desired. The conversions occur correctly.

When pitch is > 0, the coordinate conversions appear to work as expected as well. However, I think Mapbox GL JS takes the canvas, and turns it into a WebGL texture. This texture is then somehow scaled/rotated which turns the previous rectangle canvas into a trapezoid.

I need to be able to include this trapezoid scaling or account for this distortion in the canvas calculations.

Rough Sample

The snippet below is an attempt at replicating what is going on with a far simpler codebase. It's based on https://docs.mapbox.com/mapbox-gl-js/example/canvas-source/. You'll see the circles moving around, turning into ovals at different points and stretching--they should remain circles. This is partly caused by the map.unproject() calls using the screen dimensions, which are used to determine the map bounds. In 2d space, this works well because the screen bounds are identical to the camera view. But I think in 3d space, the screen dimensions are distorted by the camera view, and so the relation is lost between screen--camera--map which causes the skew.

My goal is to either correct the skew, or somehow use the camera properties to create the bounds (and project/unproject), OR there's another way to solve this. Essentially I want what is on the canvas at particular coordinates to stay there and not moved when pitched.

Thank you!

const PITCH = 45; // Change to 0 to see uniform shapes

// Based on https://docs.mapbox.com/mapbox-gl-js/example/canvas-source/
mapboxgl.accessToken = 'pk.eyJ1IjoiYnVzaGZpcmVpbyIsImEiOiJjazU3NGwzNGEwM2QxM2VwcWJ0djk1amFyIn0.DP3giqn33iswMWzlclr9jg';


var map = new mapboxgl.Map({
    container: 'map',
    zoom: 5,
    minZoom: 4,
    center: [95.899147, 18.088694],
    pitch: PITCH,
    style: 'mapbox://styles/mapbox/streets-v11'
});



//Animation from https://javascript.tutorials24x7.com/blog/how-to-draw-animated-circles-in-html5-canvas

var canvas = document.getElementById('canvasID');
var ctx = canvas.getContext('2d');
var circles = [];
var radius = 20;

function Circle(x, y, dx, dy, radius, color) {
    this.x = x;
    this.y = y;
    this.dx = dx;
    this.dy = dy;

    this.radius = radius;

    this.draw = function () {
        ctx.beginPath();
        ctx.arc(this.x, this.y, this.radius, 0, Math.PI * 2, false);
        ctx.strokeStyle = color;
        ctx.stroke();
    };

    this.update = function () {
        if (this.x + this.radius > 400 || this.x - this.radius < 0) {
            this.dx = -this.dx;
        }

        if (this.y + this.radius > 400 || this.y - this.radius < 0) {
            this.dy = -this.dy;
        }

        this.x += this.dx;
        this.y += this.dy;

        this.draw();
    };
}

for (var i = 0; i < 5; i++) {
    var color =
        '#' +
        (0x1000000 + Math.random() * 0xffffff).toString(16).substr(1, 6);
    var x = Math.random() * (400 - radius * 2) + radius;
    var y = Math.random() * (400 - radius * 2) + radius;

    var dx = (Math.random() - 0.5) * 2;
    var dy = (Math.random() - 0.5) * 2;

    circles.push(new Circle(x, y, dx, dy, radius, color));
}

function animate() {
    requestAnimationFrame(animate);
    ctx.clearRect(0, 0, 400, 400);

    for (var r = 0; r < 5; r++) {
        circles[r].update();
    }
}

animate();


let size = this.map.getContainer();
const nw = map.unproject([0, 0]);
const ne = map.unproject([size.clientWidth, 0]);
const se = map.unproject([size.clientWidth, size.clientHeight]);
const sw = map.unproject([0, size.clientHeight]);


map.on('load', function () {
    map.addSource('canvas-source', {
        type: 'canvas',
        canvas: 'canvasID',
        coordinates: [
            [nw.lng, nw.lat],
            [ne.lng, ne.lat],
            [se.lng, se.lat],
            [sw.lng, sw.lat],
        ],
        animate: true
    });

    map.addLayer({
        id: 'canvas-layer',
        type: 'raster',
        source: 'canvas-source'
    });
});
body { margin: 0; padding: 0; }
#map { position: absolute; top: 0; bottom: 0; width: 100%; }
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<title>Canvas Demo</title>
<meta name="viewport" content="initial-scale=1,maximum-scale=1,user-scalable=no">
<link href="https://api.mapbox.com/mapbox-gl-js/v2.3.1/mapbox-gl.css" rel="stylesheet">
<script src="https://api.mapbox.com/mapbox-gl-js/v2.3.1/mapbox-gl.js"></script>
</head>
<body>
<canvas id="canvasID" width="400" height="400">Canvas not supported</canvas>
<div id="map"></div>
</body>
</html>

0 Answers
Related