This example uses OpenLayers v10.9.0. The latest is v.
Edit

#### PMTiles Shaded Relief

pmtiles 2 elevation 1 datatiles 2

#####

elevation |
location |
vertical exaggeration: |  | x
sun elevation: |  | °
sun azimuth: |  | °

Elevation data from PMTiles.

This example shows a shaded relief rendering of elevation data from a PMTiles source.

##### main.js

    import Map from 'ol/Map.js';
    import View from 'ol/View.js';
    import TileLayer from 'ol/layer/WebGLTile.js';
    import {useGeographic} from 'ol/proj.js';
    import DataTile from 'ol/source/DataTile.js';
    import {PMTiles} from 'pmtiles';

    useGeographic();

    const tiles = new PMTiles(
      'https://pub-9288c68512ed46eca46ddcade307709b.r2.dev/protomaps-sample-datasets/terrarium_z9.pmtiles',
    );

    function loadImage(src) {
      return new Promise((resolve, reject) => {
        const img = new Image();
        img.addEventListener('load', () => resolve(img));
        img.addEventListener('error', () => reject(new Error('load failed')));
        img.src = src;
      });
    }

    async function loader(z, x, y, {signal}) {
      const response = await tiles.getZxy(z, x, y, signal);
      const blob = new Blob([response.data]);
      const src = URL.createObjectURL(blob);
      const image = await loadImage(src);
      URL.revokeObjectURL(src);
      return image;
    }

    // The method used to extract elevations from the DEM.
    //   red * 256 + green + blue / 256 - 32768
    function elevation(xOffset, yOffset) {
      const red = ['band', 1, xOffset, yOffset];
      const green = ['band', 2, xOffset, yOffset];
      const blue = ['band', 3, xOffset, yOffset];

      // band math operates on normalized values from 0-1
      // so we scale by 255
      return [
        '+',
        ['*', 255 * 256, red],
        ['*', 255, green],
        ['*', 255 / 256, blue],
        -32768,
      ];
    }

    // Generates a shaded relief image given elevation data.  Uses a 3x3
    // neighborhood for determining slope and aspect.
    const dp = ['*', 2, ['resolution']];
    const z0x = ['*', ['var', 'vert'], elevation(-1, 0)];
    const z1x = ['*', ['var', 'vert'], elevation(1, 0)];
    const dzdx = ['/', ['-', z1x, z0x], dp];
    const z0y = ['*', ['var', 'vert'], elevation(0, -1)];
    const z1y = ['*', ['var', 'vert'], elevation(0, 1)];
    const dzdy = ['/', ['-', z1y, z0y], dp];
    const slope = ['atan', ['sqrt', ['+', ['^', dzdx, 2], ['^', dzdy, 2]]]];
    const aspect = ['clamp', ['atan', ['-', 0, dzdx], dzdy], -Math.PI, Math.PI];
    const sunEl = ['*', Math.PI / 180, ['var', 'sunEl']];
    const sunAz = ['*', Math.PI / 180, ['var', 'sunAz']];

    const incidence = [
      '+',
      ['*', ['sin', sunEl], ['cos', slope]],
      ['*', ['cos', sunEl], ['sin', slope], ['cos', ['-', sunAz, aspect]]],
    ];
    const scaled = ['*', 255, incidence];

    const variables = {};

    const layer = new TileLayer({
      source: new DataTile({
        loader,
        wrapX: true,
        maxZoom: 9,
        attributions:
          "<a href='https://github.com/tilezen/joerd/blob/master/docs/attribution.md#attribution'>Tilezen Jörð</a>",
      }),
      style: {
        variables: variables,
        color: ['color', scaled],
      },
    });

    const controlIds = ['vert', 'sunEl', 'sunAz'];
    controlIds.forEach(function (id) {
      const control = document.getElementById(id);
      const output = document.getElementById(id + 'Out');
      function updateValues() {
        output.innerText = control.value;
        variables[id] = Number(control.value);
      }
      updateValues();
      control.addEventListener('input', function () {
        updateValues();
        layer.updateStyleVariables(variables);
      });
    });

    const map = new Map({
      target: 'map',
      layers: [layer],
      view: new View({
        center: [0, 0],
        zoom: 1,
      }),
    });

    function getElevation(data) {
      const red = data[0];
      const green = data[1];
      const blue = data[2];
      return red * 256 + green + blue / 256 - 32768;
    }

    function formatLocation([lon, lat]) {
      const NS = lat < 0 ? 'S' : 'N';
      const EW = lon < 0 ? 'W' : 'E';
      return `${Math.abs(lat).toFixed(1)}° ${NS}, ${Math.abs(lon).toFixed(
        1,
      )}° ${EW}`;
    }

    const elevationOut = document.getElementById('elevationOut');
    const locationOut = document.getElementById('locationOut');
    function displayPixelValue(event) {
      const data = layer.getData(event.pixel);
      if (!data) {
        return;
      }
      elevationOut.innerText = getElevation(data).toLocaleString() + ' m';
      locationOut.innerText = formatLocation(event.coordinate);
    }
    map.on(['pointermove', 'click'], displayPixelValue);

##### index.html

    <!DOCTYPE html>
    <html lang="en">
      <head>
        <meta charset="UTF-8">
        <title>PMTiles Shaded Relief</title>
        <link rel="stylesheet" href="node_modules/ol/ol.css">
        <style>
          .map {
            width: 100%;
            height: 400px;
          }
          table.controls td {
            padding: 2px 5px;
          }
          table.controls td:nth-child(3) {
            text-align: right;
            min-width: 3em;
          }
        </style>
      </head>
      <body>
        <div id="map" class="map"></div>
        <table class="controls">
          <tr>
            <td>elevation</td>
            <td colspan="2" id="elevationOut"></td>
          </tr>
          <tr>
            <td>location</td>
            <td colspan="2" id="locationOut"></td>
          </tr>
          <tr>
            <td><label for="vert">vertical exaggeration:</label></td>
            <td><input id="vert" type="range" min="5" max="50" value="10"/></td>
            <td><span id="vertOut"></span> x</td>
          </tr>
          <tr>
            <td><label for="sunEl">sun elevation:</label></td>
            <td><input id="sunEl" type="range" min="0" max="90" value="45"/></td>
            <td><span id="sunElOut"></span> °</td>
          </tr>
          <tr>
            <td><label for="sunAz">sun azimuth:</label></td>
            <td><input id="sunAz" type="range" min="0" max="360" value="45"/></td>
            <td><span id="sunAzOut"></span> °</td>
          </tr>
        </table>

        <script type="module" src="main.js"></script>
      </body>
    </html>

##### package.json

    {
      "name": "pmtiles-elevation",
      "dependencies": {
        "ol": "10.9.0",
        "pmtiles": "^4.0.1"
      },
      "devDependencies": {
        "vite": "^3.2.3"
      },
      "scripts": {
        "start": "vite",
        "build": "vite build"
      }
    }