0

I have made this codepen where I manipulate the context of the basemap of a OpenLayers map on postcompose. The problem I have is translating this working demo to a Vue SPA which uses OpenLayers 6 where I would like to link the dark mode map effect to a toggle. The postcompose event is not fired the same way for OL6 as it did for OL3 so I tried hooking onto prerender.

import TileLayer from 'ol/layer/Tile';
...
osm: new TileLayer({source: new OSM()})

In OpenLayers 3 the following code gets me a permanently dark mode map :

OSM_LAYER.on('postcompose', function (evt) {
    evt.context.globalCompositeOperation = 'color';
    evt.context.fillStyle = 'rgba(0,0,0,' + 1.0 + ')';
    evt.context.fillRect(0, 0, evt.context.canvas.width, evt.context.canvas.height);
    evt.context.globalCompositeOperation = 'overlay';
    evt.context.fillStyle = 'rgb(' + [200,200,200].toString() + ')';
    evt.context.fillRect(0, 0, evt.context.canvas.width, evt.context.canvas.height);
    evt.context.globalCompositeOperation = 'source-over';
    document.querySelector('canvas').style.filter="invert(99%)";
});

Currently I am trying to use getVectorContext as per this answer but I am not understanding something since my code does not give me the correct dark mode :

OSM_LAYER.on('prerender', function (evt) {
    var ctx = getVectorContext(evt).context_
    ctx.globalCompositeOperation = ‘color’;
    ctx.fillStyle = ‘rgba(0,0,0,’ + 1.0 + ‘)’;
    ctx.fillRect(0, 0, ctx.canvas.width, ctx.canvas.height);
    ctx.globalCompositeOperation = ‘overlay’;
    ctx.fillStyle = ‘rgb(‘ + [200,200,200].toString() + ‘)’;
    ctx.fillRect(0, 0, ctx.canvas.width, ctx.canvas.height);
    ctx.globalCompositeOperation = ‘source-over’;
    document.querySelector('canvas').style.filter="invert(99%)";
});

EDIT : Thanks to Mr. Mike's answer I am able to replicate the look of the OL3 dark mode. The problem now is that I actually use this basemap to create an animation where I update the time parameters of all layers in a for loop and then extract the map canvas using :

getMapCanvas () {
    var mapCanvas = document.createElement('canvas');
    var divElement = document.querySelector(".map");
    mapCanvas.width = divElement.offsetWidth;//size[0];
    mapCanvas.height = divElement.offsetHeight;//size[1];
    var mapContext = mapCanvas.getContext('2d');
    Array.prototype.forEach.call(
        document.querySelectorAll('.ol-layer canvas'),
        function (canvas) {
            if (canvas.width > 0) {
                const opacity = canvas.parentNode.style.opacity;
                mapContext.globalAlpha = opacity === '' ? 1 : Number(opacity);
                const transform = canvas.style.transform;
                const matrix = transform
                                        .match(/^matrix\(([^\(]*)\)$/)[1] //eslint-disable-line
                                        .split(',')
                                        .map(Number);
                CanvasRenderingContext2D.prototype.setTransform.apply(mapContext,matrix);
                mapContext.drawImage(canvas, 0, 0);
            }
        }
    );
    return mapCanvas;
},

which is called only after a rendercomplete promise is resolved. Using Mr. Mike's method I get a weird white hue instead of the dark mode OSM and I think it is because of how I am exporting the canvas in the getMapCanvas function. Can someone help me out debug this?

Curious
  • 383
  • 3
  • 13
  • You do not need the vector context. Just change `'postcompose'` to `'postrender'`, or use `OSM_LAYER.on(['postcompose','postrender'], function (evt) {` to work with any version of Openlayers. – Mike May 16 '22 at 17:27
  • Thank you so much for the feedback Mr. Mike, however I need to change the basemap before the `rendercomplete` event is fired. I am making an animation where in a for loop I update the times of each layer and so I need to change the style of the canvas before each step which at the moment produces a weird white hue instead of the dark map that I see before the update. – Curious May 16 '22 at 18:03
  • `postrender` fires before `rendercomplete`. You should not use `prerender` – Mike May 16 '22 at 21:21
  • I have made a new question detailing the problem I have [here](https://stackoverflow.com/q/72267583/13224380). If I understand correctly the way I export the map canvas and the way I modify the base layer on `postrender` interfere and result in this white hue on the output. – Curious May 17 '22 at 02:39

1 Answers1

1

Style filters only affect how the canvas appears on screen. To invert what is drawn needs a difference globalCompositeOperation with white fill.

this.map.getLayers().getArray()[0].on(['postrender'], (evt) => {
    if ( this.darkOSM !== null && this.darkOSM === false ) {
        evt.context.globalCompositeOperation = 'color';
        evt.context.fillStyle = 'rgba(0,0,0,' + 1.0 + ')';
        evt.context.fillRect(0, 0, evt.context.canvas.width, evt.context.canvas.height);
        evt.context.globalCompositeOperation = 'overlay';
        evt.context.fillStyle = 'rgb(' + [200,200,200].toString() + ')';
        evt.context.fillRect(0, 0, evt.context.canvas.width, evt.context.canvas.height);
        evt.context.globalCompositeOperation = 'difference';
        evt.context.fillStyle = 'rgba(255,255,255,' + 0.9 + ')';
        evt.context.fillRect(0, 0, evt.context.canvas.width, evt.context.canvas.height);
        evt.context.globalCompositeOperation = 'source-over';
    } 
});

To use on and un you would need:

this.myFunction = (evt) => {
    if ( this.darkOSM !== null && this.darkOSM === false ) {
        evt.context.globalCompositeOperation = 'color';
        evt.context.fillStyle = 'rgba(0,0,0,' + 1.0 + ')';
        evt.context.fillRect(0, 0, evt.context.canvas.width, evt.context.canvas.height);
        evt.context.globalCompositeOperation = 'overlay';
        evt.context.fillStyle = 'rgb(' + [200,200,200].toString() + ')';
        evt.context.fillRect(0, 0, evt.context.canvas.width, evt.context.canvas.height);
        evt.context.globalCompositeOperation = 'difference';
        evt.context.fillStyle = 'rgba(255,255,255,' + 0.9 + ')';
        evt.context.fillRect(0, 0, evt.context.canvas.width, evt.context.canvas.height);
        evt.context.globalCompositeOperation = 'source-over';
    } 
};
this.map.getLayers().getArray()[0].on(['postrender'], this.myFunction);
this.map.getLayers().getArray()[0].un(['postrender'], this.myFunction);

If you save a key from on it must be used with unByKey https://openlayers.org/en/latest/apidoc/module-ol_Observable.html#.unByKey

Mike
  • 16,042
  • 2
  • 14
  • 30
  • Thank you so much Mr. Mike that works like a charm. Is there a way to revert to the original style? I have a dark mode button and I would like it to toggle the style of the map as well, but for the moment I can turn the dark mode on using the `on` `postrender` event listener but I can not revert it back? – Curious May 17 '22 at 13:27
  • Quick question as the solution is good enough but it seems the `invert(99%)` style now makes everything white but it used to help the map get sligthly darker. Is there a style property that would allow me to achieve what `invert(99%)` did in [this exmaple](https://codepen.io/StuckDuckF/pen/oNEYPQg)? – Curious May 17 '22 at 13:49
  • 1
    The `difference` operation is inverting the image. If you add an invert style you (visually) undoing that If you need 99% change the alpha of the white fill to 0.99. – Mike May 17 '22 at 13:55
  • To use `un` you need to declare a function so both `on` and `un` use the same declaration. – Mike May 17 '22 at 13:57
  • I have tried this and even tried to store the `key` of said function which I passed to `un` but it did not do the trick. I suspect it is because I used `on('postrender', (evt) => this.myFunction(evt))` and the signature of `(evt) => this.myFunction(evt)` and `this.myFunction(evt)` are not the same, but I am not sure how to pass the `evt` to my function otherwise. – Curious May 17 '22 at 14:09
  • You need to pass the entire function definition, I have updated the answer. – Mike May 17 '22 at 14:55