Rendering Maps on devices where Dark Mode has been selected

1.4k Views Asked by At

I am rendering OSM map tiles onto a web page using HTML canvas drawImage. However where an end user has selected dark mode, I would like to reduce the luminosity of these displayed maps, yet still allow them to make sense to the user.

So far I have had moderate success, as follows:

  1. First plotting the map tile using drawImage
  2. setting globalCompositeOperation to "difference"
  3. over plotting the map tile with a white rectangle of the same size
  4. setting globalCompositeOperation back to "source-over"

But this simple colour inversion is not perhaps the best solution. Does anyone have any other suggestions.

3

There are 3 best solutions below

2
scai On

You could switch to a different tile server with a different map style. Check for example "CartoDB.DarkMatter" from Leaflet Provider Demo or MapBox Light & Dark.

0
Steve Brooker On

I have found a pretty good solution to this and it is as follows:

  1. First set the canvas context filter to "hue-rotate(180deg)"
  2. Then plot the map tile on the canvas using drawImage
  3. Then set the canvas context filter to "none"
  4. The set canvas context globalCompositeOperation to "difference"
  5. Then over plot the map tile with a white rectangle of the same size
  6. Finally set canvas context globalCompositeOperation back to "source-over"
0
wiedehopf On

Maybe someone will still find this useful, it's some code i'm using for this purpose in my tar1090 project. Negative and positive contrast are probably clear and dim is basically just a brightness modification with inverted sign.

toggle function:

function setDim(layer, state) {
    if (state) {
        layer.dimKey = layer.on('postrender', dim);
    } else {
        ol.Observable.unByKey(layer.dimKey);
    }
    OLMap.render();
}

postrender function:

function dim(evt) {
    const dim = mapDimPercentage * (1 + 0.25 * toggles['darkerColors'].state);
    const contrast = mapContrastPercentage * (1 + 0.1 * toggles['darkerColors'].state);
    if (dim > 0.0001) {
        evt.context.globalCompositeOperation = 'multiply';
        evt.context.fillStyle = 'rgba(0,0,0,'+dim+')';
        evt.context.fillRect(0, 0, evt.context.canvas.width, evt.context.canvas.height);
    } else if (dim < -0.0001) {
        evt.context.globalCompositeOperation = 'screen';
        console.log(evt.context.globalCompositeOperation);
        evt.context.fillStyle = 'rgba(255, 255, 255,'+(-dim)+')';
        evt.context.fillRect(0, 0, evt.context.canvas.width, evt.context.canvas.height);
    }
    if (contrast > 0.0001) {
        evt.context.globalCompositeOperation = 'overlay';
        evt.context.fillStyle = 'rgba(0,0,0,'+contrast+')';
        evt.context.fillRect(0, 0, evt.context.canvas.width, evt.context.canvas.height);
    } else if (contrast < -0.0001) {
        evt.context.globalCompositeOperation = 'overlay';
        evt.context.fillStyle = 'rgba(255, 255, 255,'+ (-contrast)+')';
        evt.context.fillRect(0, 0, evt.context.canvas.width, evt.context.canvas.height);
    }
    evt.context.globalCompositeOperation = 'source-over';
}

toggle function when using LayerSwitcher:

function setDimLayerSwitcher(state) {
    if (!state) {
        ol.control.LayerSwitcher.forEachRecursive(layers_group, function(lyr) {
                if (lyr.get('type') != 'base')
                return;
                ol.Observable.unByKey(lyr.dimKey);
                });
    } else {
        ol.control.LayerSwitcher.forEachRecursive(layers_group, function(lyr) {
                if (lyr.get('type') != 'base')
                return;
                lyr.dimKey = lyr.on('postrender', dim);
                });
    }
    OLMap.render();
}