← 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.
<!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><om-effect></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>