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

#### NDVI from a Sentinel 2 COG

cog 15 ndvi 3

#####

NDVI:

Calculating NDVI and applying a custom color map.

The GeoTIFF layer in this example draws from two Sentinel 2 sources: a red band and a near-infrared band. The layer style includes a `color` expression that calculates the Normalized Difference Vegetation Index (NDVI) from values in the two bands. The `interpolate` expression is used to map NDVI values to colors. The `layer.getData()` method can be used to retrieve pixel values from the GeoTIFF. Move your mouse or tap on the map to see calculated NDVI values based on the red and near-infrared pixel values.

##### main.js

    import Map from 'ol/Map.js';
    import TileLayer from 'ol/layer/WebGLTile.js';
    import GeoTIFF from 'ol/source/GeoTIFF.js';

    const source = new GeoTIFF({
      sources: [
        {
          // visible red, band 1 in the style expression above
          url: 'https://sentinel-cogs.s3.us-west-2.amazonaws.com/sentinel-s2-l2a-cogs/36/Q/WD/2020/7/S2A_36QWD_20200701_0_L2A/B04.tif',
          max: 10000,
        },
        {
          // near infrared, band 2 in the style expression above
          url: 'https://sentinel-cogs.s3.us-west-2.amazonaws.com/sentinel-s2-l2a-cogs/36/Q/WD/2020/7/S2A_36QWD_20200701_0_L2A/B08.tif',
          max: 10000,
        },
      ],
    });

    const layer = new TileLayer({
      style: {
        color: [
          'interpolate',
          ['linear'],
          // calculate NDVI, bands come from the sources below
          ['/', ['-', ['band', 2], ['band', 1]], ['+', ['band', 2], ['band', 1]]],
          // color ramp for NDVI values, ranging from -1 to 1
          -0.2,
          [191, 191, 191],
          -0.1,
          [219, 219, 219],
          0,
          [255, 255, 224],
          0.025,
          [255, 250, 204],
          0.05,
          [237, 232, 181],
          0.075,
          [222, 217, 156],
          0.1,
          [204, 199, 130],
          0.125,
          [189, 184, 107],
          0.15,
          [176, 194, 97],
          0.175,
          [163, 204, 89],
          0.2,
          [145, 191, 82],
          0.25,
          [128, 179, 71],
          0.3,
          [112, 163, 64],
          0.35,
          [97, 150, 54],
          0.4,
          [79, 138, 46],
          0.45,
          [64, 125, 36],
          0.5,
          [48, 110, 28],
          0.55,
          [33, 97, 18],
          0.6,
          [15, 84, 10],
          0.65,
          [0, 69, 0],
        ],
      },
      source: source,
    });

    const map = new Map({
      target: 'map',
      layers: [layer],
      view: source.getView(),
    });

    const output = document.getElementById('output');
    function displayPixelValue(event) {
      const data = layer.getData(event.pixel);
      if (!data) {
        return;
      }
      const red = data[0];
      const nir = data[1];
      const ndvi = (nir - red) / (nir + red);
      output.textContent = ndvi.toFixed(2);
    }
    map.on(['pointermove', 'click'], displayPixelValue);

##### index.html

    <!DOCTYPE html>
    <html lang="en">
      <head>
        <meta charset="UTF-8">
        <title>NDVI from a Sentinel 2 COG</title>
        <link rel="stylesheet" href="node_modules/ol/ol.css">
        <style>
          .map {
            width: 100%;
            height: 400px;
          }
        </style>
      </head>
      <body>
        <div id="map" class="map"></div>
        <div>NDVI: <span id="output"></span></div>

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

##### package.json

    {
      "name": "cog-math",
      "dependencies": {
        "ol": "10.9.0"
      },
      "devDependencies": {
        "vite": "^3.2.3"
      },
      "scripts": {
        "start": "vite",
        "build": "vite build"
      }
    }