NIKA OnlyMapJS Latest release
Discord GitHub Docs

← OnlyMapJS Gallery / Styling

Apply a print finish

Post-process finishes as markup: swap named presets on one <om-effect>, and watch each one print the op list it expands to.

Open full screen ↗ View source ↗
<!DOCTYPE html>
<!--
  A post-process finish is the last thing that happens to a frame: deck draws
  the map, then <om-effect> passes run over the finished picture. Because the
  elements ARE deck's effects array, document order is pipeline order — the
  same rule <om-layer> follows.

  Three lanes, and this page shows all three. A named `preset` is the one most
  authors want. `op=` with knobs is the same thing one step down, which is
  literally what a preset expands to — the buttons below print that expansion
  so you can see there is no hidden machinery. An inline ShaderModule is the
  escape hatch to raw GLSL.

  The knobs that describe SIZE are in millimetres, not pixels, and that is the
  whole point: a grain keyed to device pixels gets four times finer on a 4x
  print plate, which is exactly where you'd notice it going. Millimetres
  survive the trip.
-->
<html>
<head>
  <meta charset="utf-8" />
  <meta name="viewport" content="width=device-width, initial-scale=1" />
  <title>OnlyMapJS — Apply a print finish</title>
  <link rel="stylesheet" href="https://unpkg.com/@nika-js/onlymap/dist/onlymapjs.css" />
  <script type="module" src="https://unpkg.com/@nika-js/onlymap/dist/onlymap.standalone.js"></script>
  <style>
    :root { color-scheme: dark; }
    html, body { margin: 0; height: 100%; font-family: system-ui, sans-serif; }
    om-map { display: block; height: 100vh; }
    .panel {
      position: absolute; z-index: 10; top: 12px; left: 12px; width: 268px;
      padding: 14px 16px; border-radius: 10px;
      background: rgba(20, 22, 28, .93); color: #f8fafc;
      font-size: 13px; line-height: 1.45; box-shadow: 0 6px 24px rgba(0,0,0,.35);
    }
    .panel h1 { margin: 0 0 6px; font-size: 14px; }
    .panel p { margin: 0 0 10px; color: #aab2c5; }
    .looks { display: flex; flex-wrap: wrap; gap: 6px; margin-bottom: 10px; }
    .looks button {
      flex: 1 1 auto; padding: 6px 9px; border-radius: 6px; cursor: pointer;
      border: 1px solid #39405260; background: #222634; color: #e5e9f2; font: inherit;
    }
    .looks button[aria-pressed="true"] { background: #3b82f6; border-color: #3b82f6; color: #fff; }
    pre {
      margin: 0; padding: 9px 10px; border-radius: 6px; overflow-x: auto;
      background: #0e1017; color: #cbd5e1; font-size: 11px; line-height: 1.4;
    }
    .note { margin-top: 9px; color: #8b93a7; font-size: 11.5px; }
  </style>
</head>
<body>

<div class="panel">
  <h1>Apply a print finish</h1>
  <p>Each look is a <code>preset</code> on one <code>&lt;om-effect&gt;</code>. A preset is only a saved op list — the markup below is what it expands to.</p>
  <div class="looks" id="looks"></div>
  <pre id="expansion">…</pre>
  <div class="note">Spatial knobs are millimetres, so the look keeps its physical size at print DPI.</div>
</div>

<om-map center="[-98.5, 39.5]" zoom="3.6" basemap="none" validate>
  <!-- basemap="none": a post-process pass runs over deck's own framebuffer,
       so a MapLibre basemap would composite UNDERNEATH it and stay untreated.
       Drawing the base as layers keeps the whole picture inside the effect. -->
  <om-layer id="land" type="GeoJsonLayer" data="../../data/ne-countries/ne_110m_admin_0_countries.shp"
            label="Land" filled stroked color="#2a3142"
            get-line-color="[70, 80, 100]" line-width-min-pixels="0.6"></om-layer>

  <om-layer id="cities" type="ScatterplotLayer" data="../../data/us-cities.json"
            label="Cities" pickable
            get-position="[$lon, $lat]"
            get-fill-color="scale($population, sequential, ['#4b7bd1','#f2c14e'], domain=[50000, 8000000])"
            get-radius="scale($population, sqrt, [3, 26], domain=[50000, 8000000])"
            radius-units="pixels"></om-layer>

  <!-- The finish. Swapped live by the buttons; document order is pipeline order. -->
  <om-effect id="finish" preset="vintage"></om-effect>

  <om-widget type="legend" position="top-right"></om-widget>
</om-map>

<script type="module">
  import { expandPreset, EFFECT_PRESET_NAMES } from "https://unpkg.com/@nika-js/onlymap/dist/onlymap.standalone.js";

  const looks = document.getElementById("looks");
  const out = document.getElementById("expansion");
  const effect = document.getElementById("finish");

  const show = (name) => {
    for (const b of looks.children) b.setAttribute("aria-pressed", String(b.dataset.name === name));
    // `expandPreset` is the eject button: it prints the preset as the ops it
    // stands for, ready to paste in place of the preset attribute and tune.
    out.textContent = name === "none" ? "<!-- no effect -->" : expandPreset(name);
  };

  for (const name of ["none", ...EFFECT_PRESET_NAMES]) {
    const b = document.createElement("button");
    b.textContent = name;
    b.dataset.name = name;
    b.addEventListener("click", () => {
      if (name === "none") effect.removeAttribute("preset");
      else effect.setAttribute("preset", name);
      show(name);
    });
    looks.appendChild(b);
  }
  show("vintage");
</script>
</body>
</html>